|
1 | 1 | <script setup> |
| 2 | + import { Copy, FileText, Terminal } from "lucide-vue-next" |
2 | 3 | import { computed, onMounted, onUnmounted, ref } from "vue" |
3 | | - import { Terminal, FileText, Copy } from "lucide-vue-next" |
4 | 4 |
|
5 | 5 | // ── Raw Python snippet imports ────────────────────────────────────────────── |
6 | 6 | import applicationRaw from "../snippets/application.py?raw" |
7 | 7 | import artisanRaw from "../snippets/artisan?raw" |
| 8 | + import databaseAppRaw from "../snippets/database_app.py?raw" |
8 | 9 | import dbMigrationsRaw from "../snippets/database_migrations.py?raw" |
9 | 10 | import dbModelsRaw from "../snippets/database_models.py?raw" |
| 11 | + import fastapiAppRaw from "../snippets/fastapi-app.py?raw" |
10 | 12 | import fastapiRaw from "../snippets/fastapi.py?raw" |
11 | | - import fastapiControllerRaw from "../snippets/fastapi_users_controller.py?raw" |
12 | 13 | import fastapiRoutesRaw from "../snippets/fastapi_routes.py?raw" |
| 14 | + import fastapiControllerRaw from "../snippets/fastapi_users_controller.py?raw" |
13 | 15 | import logLoggerRaw from "../snippets/logging_logger.py?raw" |
14 | 16 |
|
15 | 17 | // ── Word rotator ──────────────────────────────────────────────────────────── |
|
32 | 34 | onUnmounted(() => { if (interval) clearInterval(interval) }) |
33 | 35 |
|
34 | 36 | // ── Tab / file structure ──────────────────────────────────────────────────── |
35 | | - const categories = ["Application", "FastAPI", "Database", "Migrations", "Logging"] |
| 37 | + const categories = ["Application", "FastAPI", "Database", "Logging"] |
36 | 38 | const activeCategory = ref("Application") |
37 | 39 | const activeFileIndex = ref(0) |
38 | 40 | const isTransitioning = ref(false) |
39 | 41 |
|
40 | 42 | const tabData = { |
41 | 43 | Application: { files: ["application.py", "artisan"], raw: [applicationRaw, artisanRaw] }, |
42 | | - FastAPI: { files: ["fastapi.py", "users_controllers.py", "api.py"], raw: [fastapiRaw, fastapiControllerRaw, fastapiRoutesRaw] }, |
43 | | - Database: { files: ["models.py"], raw: [dbModelsRaw] }, |
44 | | - Migrations: { files: ["2026_04_26_110113_create_users.py"], raw: [dbMigrationsRaw] }, |
| 44 | + FastAPI: { files: ["api.py", "application", "api.py", "users_controllers.py"], raw: [fastapiRaw, fastapiAppRaw, fastapiRoutesRaw, fastapiControllerRaw] }, |
| 45 | + Database: { files: ["application.py", "2026_04_26_110113_create_users.py", "models.py"], raw: [databaseAppRaw, dbMigrationsRaw, dbModelsRaw] }, |
45 | 46 | Logging: { files: ["logging.py"], raw: [logLoggerRaw] }, |
46 | 47 | } |
47 | 48 |
|
|
50 | 51 | const highlighted = ref({}) |
51 | 52 | const highlighterReady = ref(false) |
52 | 53 |
|
| 54 | + // Strip diff markers from source and return { code, lineTypes } |
| 55 | + // Supported markers: # [+] # [-] # [!hl] |
| 56 | + function extractHighlights(raw) { |
| 57 | + const lines = raw.split("\n") |
| 58 | + const lineTypes = {} // index -> 'add' | 'remove' | 'highlight' |
| 59 | + const cleaned = lines.map((line, i) => { |
| 60 | + if (line.includes("# [+]")) { |
| 61 | + lineTypes[i] = "add" |
| 62 | + return line.replace(/\s*#\s*\[\+\]/, "") |
| 63 | + } |
| 64 | + if (line.includes("# [-]")) { |
| 65 | + lineTypes[i] = "remove" |
| 66 | + return line.replace(/\s*#\s*\[-\]/, "") |
| 67 | + } |
| 68 | + return line |
| 69 | + }) |
| 70 | + return { code: cleaned.join("\n"), lineTypes } |
| 71 | + } |
| 72 | +
|
53 | 73 | onMounted(async () => { |
54 | 74 | try { |
55 | 75 | const { createHighlighter } = await import("shiki") |
|
60 | 80 |
|
61 | 81 | const result = {} |
62 | 82 | for (const [cat, data] of Object.entries(tabData)) { |
63 | | - result[cat] = data.raw.map(code => { |
| 83 | + result[cat] = data.raw.map(raw => { |
| 84 | + const { code, lineTypes } = extractHighlights(raw) |
64 | 85 | const html = hl.codeToHtml(code, { lang: "python", theme: "github-dark" }) |
65 | 86 | // Strip outer <pre ...><code> wrapper — we render inside our own pre/code |
66 | | - return html |
| 87 | + const inner = html |
67 | 88 | .replace(/^<pre[^>]*><code[^>]*>/, "") |
68 | 89 | .replace(/<\/code><\/pre>$/, "") |
| 90 | + // Wrap each line in a span and apply diff/highlight classes |
| 91 | + return inner.split("\n").map((line, i) => { |
| 92 | + const type = lineTypes[i] |
| 93 | + const cls = type ? ` ${type}` : "" |
| 94 | + const prefix = type === "add" ? "+" : type === "remove" ? "-" : " " |
| 95 | + return `<span class="line${cls}"><span class="diff-sign">${prefix}</span>${line}</span>` |
| 96 | + }).join("\n") |
69 | 97 | }) |
70 | 98 | } |
71 | 99 | highlighted.value = result |
|
111 | 139 | }, 150) |
112 | 140 | } |
113 | 141 |
|
| 142 | + // ── Copy to clipboard ─────────────────────────────────────────────────────── |
| 143 | + const copied = ref(false) |
| 144 | +
|
| 145 | + function copyCode() { |
| 146 | + const raw = currentTabData.value.raw[activeFileIndex.value] |
| 147 | + const { code } = extractHighlights(raw) |
| 148 | + navigator.clipboard.writeText(code).then(() => { |
| 149 | + copied.value = true |
| 150 | + setTimeout(() => { copied.value = false }, 2000) |
| 151 | + }) |
| 152 | + } |
| 153 | +
|
114 | 154 | // ── Synchronized horizontal scroll (file tabs ↔ code body) ───────────────── |
115 | 155 | const fileTabsRef = ref(null) |
116 | 156 | const codeBodyRef = ref(null) |
|
164 | 204 | <div class="flex flex-col sm:flex-row gap-4"> |
165 | 205 | <a href="/docs/getting-started" class="bg-brand-teal text-white dark:text-black px-8 py-4 rounded font-label-md font-bold flex items-center justify-center gap-2 transition-all hover:brightness-110 shadow-lg shadow-brand-teal/20 active:scale-[0.98]"> |
166 | 206 | Initialize Project |
167 | | - <Terminal :size="18" /> |
| 207 | + <Terminal :size="18"/> |
168 | 208 | </a> |
169 | 209 | </div> |
170 | 210 |
|
|
223 | 263 | :class="activeFileIndex === idx ? 'bg-brand-teal/10 border-b-2 border-brand-teal' : 'opacity-50 hover:opacity-75'" |
224 | 264 | @click="switchFile(idx)" |
225 | 265 | > |
226 | | - <FileText :size="12" :class="activeFileIndex === idx ? 'text-brand-teal' : 'text-outline-variant'" /> |
| 266 | + <FileText :size="12" :class="activeFileIndex === idx ? 'text-brand-teal' : 'text-outline-variant'"/> |
227 | 267 | <span class="text-xs font-mono tracking-tight" :class="activeFileIndex === idx ? 'text-white' : 'text-outline-variant'">{{ file }}</span> |
228 | 268 | </button> |
229 | 269 | </div> |
230 | 270 | </div> |
231 | | - <Copy :size="14" class="text-outline-variant hover:text-brand-teal cursor-pointer transition-colors" /> |
| 271 | + <button @click="copyCode" class="shrink-0 transition-colors" :title="copied ? 'Copied!' : 'Copy code'"> |
| 272 | + <Copy v-if="!copied" :size="14" class="text-outline-variant hover:text-brand-teal"/> |
| 273 | + <span v-else class="text-[11px] text-brand-teal font-mono">copied!</span> |
| 274 | + </button> |
232 | 275 | </div> |
233 | 276 |
|
234 | 277 | <!-- Code body --> |
|
251 | 294 | pre, code { |
252 | 295 | background: transparent !important; |
253 | 296 | } |
| 297 | +
|
| 298 | + /* Diff / highlight line decorations */ |
| 299 | + :deep(.line) { |
| 300 | + display: inline-block; |
| 301 | + width: 100%; |
| 302 | + } |
| 303 | +
|
| 304 | + :deep(.diff-sign) { |
| 305 | + user-select: none; |
| 306 | + margin-right: 6px; |
| 307 | + opacity: 0.6; |
| 308 | + } |
| 309 | +
|
| 310 | + :deep(.line.highlight) { |
| 311 | + background-color: rgba(5, 150, 105, 0.15); |
| 312 | + border-left: 2px solid #059669; |
| 313 | + padding-left: 6px; |
| 314 | + margin-left: -8px; |
| 315 | + } |
| 316 | +
|
| 317 | + :deep(.line.add) { |
| 318 | + background-color: rgba(5, 150, 105, 0.15); |
| 319 | + border-left: 2px solid #059669; |
| 320 | + padding-left: 6px; |
| 321 | + margin-left: -8px; |
| 322 | + } |
| 323 | +
|
| 324 | + :deep(.line.add .diff-sign) { |
| 325 | + color: #059669; |
| 326 | + opacity: 1; |
| 327 | + } |
| 328 | +
|
| 329 | + :deep(.line.remove) { |
| 330 | + background-color: rgba(239, 68, 68, 0.1); |
| 331 | + border-left: 2px solid #ef4444; |
| 332 | + padding-left: 6px; |
| 333 | + margin-left: -8px; |
| 334 | + } |
| 335 | +
|
| 336 | + :deep(.line.remove .diff-sign) { |
| 337 | + color: #ef4444; |
| 338 | + opacity: 1; |
| 339 | + } |
254 | 340 | </style> |
0 commit comments