Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49aa173b58
|
||
|
|
999e91226b
|
||
|
|
f64365bc35
|
||
|
|
55f565c619
|
||
|
|
7067038ab5
|
||
|
|
921d73bf08
|
||
|
|
6486ce1f91
|
||
|
|
389b6354f7
|
||
|
|
cfa6cf2be2
|
No files matched your search
+1
-1
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@astrojs/svelte": "^8.0.4",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"astro": "^6.1.4",
|
||||
"astro": "^6.1.9",
|
||||
"esptool-js": "^0.6.0",
|
||||
"marked": "^18.0.0",
|
||||
"svelte": "^5.55.2",
|
||||
|
||||
Generated
+29
-26
@@ -10,13 +10,13 @@ importers:
|
||||
dependencies:
|
||||
'@astrojs/svelte':
|
||||
specifier: ^8.0.4
|
||||
version: 8.0.4(astro@6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3))(jiti@2.6.1)(lightningcss@1.32.0)(svelte@5.55.2)(typescript@5.9.3)(yaml@2.8.3)
|
||||
version: 8.0.4(astro@6.1.9(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3))(jiti@2.6.1)(lightningcss@1.32.0)(svelte@5.55.2)(typescript@5.9.3)(yaml@2.8.3)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
|
||||
astro:
|
||||
specifier: ^6.1.4
|
||||
version: 6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3)
|
||||
specifier: ^6.1.9
|
||||
version: 6.1.9(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3)
|
||||
esptool-js:
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.0
|
||||
@@ -35,11 +35,11 @@ packages:
|
||||
'@astrojs/compiler@3.0.1':
|
||||
resolution: {integrity: sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==}
|
||||
|
||||
'@astrojs/internal-helpers@0.8.0':
|
||||
resolution: {integrity: sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==}
|
||||
'@astrojs/internal-helpers@0.9.0':
|
||||
resolution: {integrity: sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==}
|
||||
|
||||
'@astrojs/markdown-remark@7.1.0':
|
||||
resolution: {integrity: sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ==}
|
||||
'@astrojs/markdown-remark@7.1.1':
|
||||
resolution: {integrity: sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==}
|
||||
|
||||
'@astrojs/prism@4.0.1':
|
||||
resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==}
|
||||
@@ -53,8 +53,8 @@ packages:
|
||||
svelte: ^5.43.6
|
||||
typescript: ^5.3.3
|
||||
|
||||
'@astrojs/telemetry@3.3.0':
|
||||
resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==}
|
||||
'@astrojs/telemetry@3.3.1':
|
||||
resolution: {integrity: sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw==}
|
||||
engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
|
||||
|
||||
'@babel/helper-string-parser@7.27.1':
|
||||
@@ -761,8 +761,8 @@ packages:
|
||||
array-iterate@2.0.1:
|
||||
resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==}
|
||||
|
||||
astro@6.1.4:
|
||||
resolution: {integrity: sha512-SRy1bONuCHkGWhI5JiWCQKVDVbeaXOikjAVZs/Nz+lvUvubtdLoZfnacmuZHQ9RL2IOkU54M8/qZYm9ypJDKrg==}
|
||||
astro@6.1.9:
|
||||
resolution: {integrity: sha512-NsAHzMzpznB281g2aM5qnBt2QjfH6ttKiZ3hSZw52If8JJ+62kbnBKbyKhR2glQcJLl7Jfe4GSl0DihFZ36rRQ==}
|
||||
engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1038,6 +1038,11 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
hasBin: true
|
||||
|
||||
is-docker@4.0.0:
|
||||
resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
is-inside-container@1.0.0:
|
||||
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -1741,13 +1746,13 @@ snapshots:
|
||||
|
||||
'@astrojs/compiler@3.0.1': {}
|
||||
|
||||
'@astrojs/internal-helpers@0.8.0':
|
||||
'@astrojs/internal-helpers@0.9.0':
|
||||
dependencies:
|
||||
picomatch: 4.0.4
|
||||
|
||||
'@astrojs/markdown-remark@7.1.0':
|
||||
'@astrojs/markdown-remark@7.1.1':
|
||||
dependencies:
|
||||
'@astrojs/internal-helpers': 0.8.0
|
||||
'@astrojs/internal-helpers': 0.9.0
|
||||
'@astrojs/prism': 4.0.1
|
||||
github-slugger: 2.0.0
|
||||
hast-util-from-html: 2.0.3
|
||||
@@ -1775,10 +1780,10 @@ snapshots:
|
||||
dependencies:
|
||||
prismjs: 1.30.0
|
||||
|
||||
'@astrojs/svelte@8.0.4(astro@6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3))(jiti@2.6.1)(lightningcss@1.32.0)(svelte@5.55.2)(typescript@5.9.3)(yaml@2.8.3)':
|
||||
'@astrojs/svelte@8.0.4(astro@6.1.9(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3))(jiti@2.6.1)(lightningcss@1.32.0)(svelte@5.55.2)(typescript@5.9.3)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.2)(vite@7.3.2(jiti@2.6.1)(lightningcss@1.32.0)(yaml@2.8.3))
|
||||
astro: 6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3)
|
||||
astro: 6.1.9(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3)
|
||||
svelte: 5.55.2
|
||||
svelte2tsx: 0.7.53(svelte@5.55.2)(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
@@ -1796,17 +1801,14 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
'@astrojs/telemetry@3.3.0':
|
||||
'@astrojs/telemetry@3.3.1':
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
debug: 4.4.3
|
||||
dlv: 1.1.3
|
||||
dset: 3.1.4
|
||||
is-docker: 3.0.0
|
||||
is-docker: 4.0.0
|
||||
is-wsl: 3.1.1
|
||||
which-pm-runs: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-string-parser@7.27.1': {}
|
||||
|
||||
@@ -2293,12 +2295,12 @@ snapshots:
|
||||
|
||||
array-iterate@2.0.1: {}
|
||||
|
||||
astro@6.1.4(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3):
|
||||
astro@6.1.9(jiti@2.6.1)(lightningcss@1.32.0)(rollup@4.60.1)(typescript@5.9.3)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 3.0.1
|
||||
'@astrojs/internal-helpers': 0.8.0
|
||||
'@astrojs/markdown-remark': 7.1.0
|
||||
'@astrojs/telemetry': 3.3.0
|
||||
'@astrojs/internal-helpers': 0.9.0
|
||||
'@astrojs/markdown-remark': 7.1.1
|
||||
'@astrojs/telemetry': 3.3.1
|
||||
'@capsizecss/unpack': 4.0.0
|
||||
'@clack/prompts': 1.2.0
|
||||
'@oslojs/encoding': 1.1.0
|
||||
@@ -2311,7 +2313,6 @@ snapshots:
|
||||
cookie: 1.1.1
|
||||
devalue: 5.7.0
|
||||
diff: 8.0.4
|
||||
dlv: 1.1.3
|
||||
dset: 3.1.4
|
||||
es-module-lexer: 2.0.0
|
||||
esbuild: 0.27.7
|
||||
@@ -2700,6 +2701,8 @@ snapshots:
|
||||
|
||||
is-docker@3.0.0: {}
|
||||
|
||||
is-docker@4.0.0: {}
|
||||
|
||||
is-inside-container@1.0.0:
|
||||
dependencies:
|
||||
is-docker: 3.0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { BoardDef } from "../lib/types";
|
||||
import { boards } from "../lib/boards";
|
||||
import { boards, badgeText, badgeColor } from "../lib/boards";
|
||||
|
||||
interface Props {
|
||||
selected: BoardDef | null;
|
||||
@@ -9,84 +9,143 @@
|
||||
|
||||
let { selected, onselect }: Props = $props();
|
||||
|
||||
const espBoards = boards.filter((b) => b.family === "esp32s3");
|
||||
const rpBoards = boards.filter((b) => b.family === "rp2040");
|
||||
const nrfBoards = boards.filter((b) => b.family === "nrf52840");
|
||||
let query = $state("");
|
||||
|
||||
function badgeText(board: BoardDef): string {
|
||||
switch (board.flashMethod) {
|
||||
case "web-serial":
|
||||
return "Web Flash";
|
||||
case "uf2":
|
||||
return "UF2 Download";
|
||||
case "download-only":
|
||||
return "CLI Only";
|
||||
}
|
||||
function matches(board: BoardDef, q: string): boolean {
|
||||
const needle = q.trim().toLowerCase();
|
||||
if (!needle) return true;
|
||||
const haystack = [
|
||||
board.name,
|
||||
board.mcu,
|
||||
board.radio,
|
||||
board.family,
|
||||
badgeText(board),
|
||||
board.notes ?? "",
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return haystack.includes(needle);
|
||||
}
|
||||
|
||||
function badgeColor(board: BoardDef): string {
|
||||
switch (board.flashMethod) {
|
||||
case "web-serial":
|
||||
return "text-accent";
|
||||
case "uf2":
|
||||
return "text-info";
|
||||
case "download-only":
|
||||
return "text-warn";
|
||||
const esp32Boards = $derived(boards.filter((b) => b.family === "esp32" && matches(b, query)));
|
||||
const espBoards = $derived(boards.filter((b) => b.family === "esp32s3" && matches(b, query)));
|
||||
const nrfBoards = $derived(boards.filter((b) => b.family === "nrf52840" && matches(b, query)));
|
||||
const rpBoards = $derived(boards.filter((b) => b.family === "rp2040" && matches(b, query)));
|
||||
|
||||
const totalVisible = $derived(
|
||||
esp32Boards.length + espBoards.length + nrfBoards.length + rpBoards.length,
|
||||
);
|
||||
const isFiltering = $derived(query.trim().length > 0);
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" && query !== "") {
|
||||
query = "";
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet boardCard(board: BoardDef)}
|
||||
<button
|
||||
class="flex flex-col h-full text-left bg-bg-card border p-4 rounded transition-all cursor-pointer
|
||||
{selected?.id === board.id
|
||||
? 'border-accent shadow-[0_0_12px_oklch(from_var(--color-accent)_l_c_h_/_0.15)]'
|
||||
: 'border-border hover:border-border-bright'}"
|
||||
onclick={() => onselect(board)}
|
||||
>
|
||||
<div class="font-mono font-medium text-text text-sm">{board.name}</div>
|
||||
<div class="text-xs text-text-muted mt-1">{board.mcu} · {board.radio}</div>
|
||||
<div class="text-xs {badgeColor(board)} mt-auto pt-2 font-mono">{badgeText(board)}</div>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet familySection(label: string, list: BoardDef[])}
|
||||
{#if list.length > 0}
|
||||
<p class="text-xs text-text-dim mb-2 font-mono">{label}</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
{#each list as board (board.id)}
|
||||
{@render boardCard(board)}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div>
|
||||
<h3 class="font-mono text-sm text-text-muted mb-3 uppercase tracking-wider">Select Board</h3>
|
||||
|
||||
<p class="text-xs text-text-dim mb-2 font-mono">ESP32-S3</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
{#each espBoards as board}
|
||||
<button
|
||||
class="text-left bg-bg-card border p-4 rounded transition-all cursor-pointer
|
||||
{selected?.id === board.id
|
||||
? 'border-accent shadow-[0_0_12px_oklch(from_var(--color-accent)_l_c_h_/_0.15)]'
|
||||
: 'border-border hover:border-border-bright'}"
|
||||
onclick={() => onselect(board)}
|
||||
>
|
||||
<div class="font-mono font-medium text-text text-sm">{board.name}</div>
|
||||
<div class="text-xs text-text-muted mt-1">{board.mcu} · {board.radio}</div>
|
||||
<div class="text-xs {badgeColor(board)} mt-2 font-mono">{badgeText(board)}</div>
|
||||
</button>
|
||||
{/each}
|
||||
<div class="flex items-baseline justify-between mb-3 gap-4">
|
||||
<h3 class="font-mono text-sm text-text-muted uppercase tracking-wider">Select Board</h3>
|
||||
{#if isFiltering}
|
||||
<p class="text-xs text-text-dim font-mono">
|
||||
{totalVisible} of {boards.length}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-text-dim mb-2 font-mono">nRF52840</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
|
||||
{#each nrfBoards as board}
|
||||
<div class="relative mb-6">
|
||||
<svg
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-dim pointer-events-none"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="11" cy="11" r="7"></circle>
|
||||
<path d="m20 20-3.5-3.5"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={query}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder="Filter by name, chip, radio…"
|
||||
aria-label="Filter boards"
|
||||
class="w-full bg-bg-card border border-border rounded pl-10 pr-10 py-2 font-mono text-sm text-text placeholder:text-text-dim focus:outline-none focus:border-accent transition-colors"
|
||||
/>
|
||||
{#if query}
|
||||
<button
|
||||
class="text-left bg-bg-card border p-4 rounded transition-all cursor-pointer
|
||||
{selected?.id === board.id
|
||||
? 'border-accent shadow-[0_0_12px_oklch(from_var(--color-accent)_l_c_h_/_0.15)]'
|
||||
: 'border-border hover:border-border-bright'}"
|
||||
onclick={() => onselect(board)}
|
||||
type="button"
|
||||
onclick={() => {
|
||||
query = "";
|
||||
}}
|
||||
aria-label="Clear filter"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-text-dim hover:text-text cursor-pointer p-1 rounded"
|
||||
>
|
||||
<div class="font-mono font-medium text-text text-sm">{board.name}</div>
|
||||
<div class="text-xs text-text-muted mt-1">{board.mcu} · {board.radio}</div>
|
||||
<div class="text-xs {badgeColor(board)} mt-2 font-mono">{badgeText(board)}</div>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18 6 6 18M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-text-dim mb-2 font-mono">RP2040</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{#each rpBoards as board}
|
||||
{#if totalVisible === 0}
|
||||
<div class="bg-bg-card border border-border rounded p-6 text-center">
|
||||
<p class="text-text-muted text-sm mb-3">
|
||||
No boards match "<span class="text-text font-mono">{query}</span>"
|
||||
</p>
|
||||
<button
|
||||
class="text-left bg-bg-card border p-4 rounded transition-all cursor-pointer
|
||||
{selected?.id === board.id
|
||||
? 'border-accent shadow-[0_0_12px_oklch(from_var(--color-accent)_l_c_h_/_0.15)]'
|
||||
: 'border-border hover:border-border-bright'}"
|
||||
onclick={() => onselect(board)}
|
||||
type="button"
|
||||
onclick={() => {
|
||||
query = "";
|
||||
}}
|
||||
class="text-accent hover:text-accent-bright text-sm font-mono cursor-pointer"
|
||||
>
|
||||
<div class="font-mono font-medium text-text text-sm">{board.name}</div>
|
||||
<div class="text-xs text-text-muted mt-1">{board.mcu} · {board.radio}</div>
|
||||
<div class="text-xs {badgeColor(board)} mt-2 font-mono">{badgeText(board)}</div>
|
||||
Clear filter
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{@render familySection("ESP32", esp32Boards)}
|
||||
{@render familySection("ESP32-S3", espBoards)}
|
||||
{@render familySection("nRF52840", nrfBoards)}
|
||||
{@render familySection("RP2040", rpBoards)}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
let session;
|
||||
try {
|
||||
session = await connect(callbacks);
|
||||
session = await connect(callbacks, board.family === "esp32" ? "esp32" : "esp32s3");
|
||||
} catch (err) {
|
||||
const msg = err instanceof DOMException && err.name === "NotFoundError"
|
||||
? "Port selection cancelled."
|
||||
@@ -227,7 +227,7 @@
|
||||
<p class="text-warn text-sm mb-2">
|
||||
.bin file not available for this release.
|
||||
<a
|
||||
href="https://github.com/donglora/firmware/releases"
|
||||
href="https://git.swaits.com/donglora/firmware/releases"
|
||||
class="text-accent hover:text-accent-bright"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { BoardDef, Release } from "../lib/types";
|
||||
import { boards } from "../lib/boards";
|
||||
import { fetchReleases } from "../lib/github";
|
||||
import BoardSelector from "./BoardSelector.svelte";
|
||||
import VersionSelector from "./VersionSelector.svelte";
|
||||
@@ -29,6 +30,14 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Pre-select board from ?board=<id> deep link (e.g. from the homepage grid)
|
||||
$effect(() => {
|
||||
const boardId = new URLSearchParams(window.location.search).get("board");
|
||||
if (!boardId) return;
|
||||
const found = boards.find((b) => b.id === boardId);
|
||||
if (found) selectedBoard = found;
|
||||
});
|
||||
|
||||
function handleBoardSelect(board: BoardDef) {
|
||||
selectedBoard = board;
|
||||
}
|
||||
@@ -59,7 +68,7 @@
|
||||
{#if selectedBoard && selectedRelease}
|
||||
<div class="border-t border-border pt-8">
|
||||
<h3 class="font-mono text-sm text-text-muted mb-3 uppercase tracking-wider">
|
||||
{#if selectedBoard.family === "esp32s3"}
|
||||
{#if (selectedBoard.family === "esp32" || selectedBoard.family === "esp32s3")}
|
||||
Flash Firmware
|
||||
{:else}
|
||||
Download Firmware
|
||||
@@ -72,7 +81,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedBoard.family === "esp32s3"}
|
||||
{#if (selectedBoard.family === "esp32" || selectedBoard.family === "esp32s3")}
|
||||
<EspFlashFlow board={selectedBoard} release={selectedRelease} />
|
||||
{:else}
|
||||
<Uf2DownloadFlow board={selectedBoard} release={selectedRelease} />
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
? "RAK4631"
|
||||
: board.id === "wio_tracker_l1"
|
||||
? "WIO-L1"
|
||||
: board.id === "waveshare_rp2040_lora"
|
||||
? "RPI-RP2"
|
||||
: "UF2BOOT",
|
||||
: board.id === "heltec_mesh_node_t114"
|
||||
? "T114BOOT"
|
||||
: board.id === "waveshare_rp2040_lora"
|
||||
? "RPI-RP2"
|
||||
: "UF2BOOT",
|
||||
);
|
||||
|
||||
function handleDownload() {
|
||||
@@ -44,7 +46,7 @@
|
||||
<p class="text-warn text-sm">
|
||||
UF2 file not found for {board.name} v{release.version}.
|
||||
<a
|
||||
href="https://github.com/donglora/firmware/releases"
|
||||
href="https://git.swaits.com/donglora/firmware/releases"
|
||||
class="text-accent hover:text-accent-bright"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -12,7 +12,17 @@
|
||||
|
||||
let { releases, selected, onselect, loading, error }: Props = $props();
|
||||
|
||||
let changelogHtml = $derived(selected?.body ? marked.parse(selected.body) : "");
|
||||
// Tag Keep-a-Changelog section headings so CSS can color-code them
|
||||
// (Added → green, Changed → yellow, Fixed → blue, etc.).
|
||||
function renderBody(body: string): string {
|
||||
const html = marked.parse(body) as string;
|
||||
return html.replace(
|
||||
/<h3>(Added|Changed|Fixed|Removed|Deprecated|Security)<\/h3>/g,
|
||||
(_, text) => `<h3 data-section="${text.toLowerCase()}">${text}</h3>`,
|
||||
);
|
||||
}
|
||||
|
||||
let changelogHtml = $derived(selected?.body ? renderBody(selected.body) : "");
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const tag = (e.target as HTMLSelectElement).value;
|
||||
@@ -30,7 +40,7 @@
|
||||
<div class="text-error text-sm">
|
||||
{error}
|
||||
<a
|
||||
href="https://github.com/donglora/firmware/releases"
|
||||
href="https://git.swaits.com/donglora/firmware/releases"
|
||||
class="text-accent hover:text-accent-bright ml-2"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -61,49 +71,14 @@
|
||||
</div>
|
||||
|
||||
{#if selected && changelogHtml}
|
||||
<div class="mt-4 bg-bg-card border border-border rounded p-4">
|
||||
<h4 class="font-mono text-xs text-text-dim uppercase tracking-wider mb-3">Changelog</h4>
|
||||
<div class="prose-dark text-sm text-text-muted leading-relaxed">
|
||||
<div class="mt-4 bg-bg-card border border-border rounded p-5">
|
||||
<h4 class="font-mono text-xs text-text-dim uppercase tracking-wider mb-4">Changelog</h4>
|
||||
<!-- Styles live in src/styles/global.css — Svelte component <style>
|
||||
scoping wasn't being emitted into the prod CSS bundle. -->
|
||||
<div class="changelog text-sm text-text-muted">
|
||||
{@html changelogHtml}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.prose-dark :global(h1),
|
||||
.prose-dark :global(h2),
|
||||
.prose-dark :global(h3) {
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.prose-dark :global(ul) {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
.prose-dark :global(li) {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.prose-dark :global(code) {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8em;
|
||||
color: var(--color-accent);
|
||||
background: var(--color-bg-elevated);
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.prose-dark :global(a) {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
.prose-dark :global(a:hover) {
|
||||
color: var(--color-accent-bright);
|
||||
}
|
||||
.prose-dark :global(p) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -34,7 +34,7 @@ const { title, description = "Protocol-agnostic LoRa over USB" } = Astro.props;
|
||||
<a href="/" class="text-text-muted hover:text-text transition-colors">Home</a>
|
||||
<a href="/flash" class="text-text-muted hover:text-text transition-colors">Flasher</a>
|
||||
<a
|
||||
href="https://github.com/donglora"
|
||||
href="https://git.swaits.com/donglora"
|
||||
class="text-text-muted hover:text-text transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -58,12 +58,12 @@ const { title, description = "Protocol-agnostic LoRa over USB" } = Astro.props;
|
||||
<p>
|
||||
MIT License ·
|
||||
<a
|
||||
href="https://github.com/donglora"
|
||||
href="https://git.swaits.com/donglora"
|
||||
class="text-text-muted hover:text-accent transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
github.com/donglora
|
||||
git.swaits.com/donglora
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+68
-10
@@ -1,26 +1,49 @@
|
||||
import type { BoardDef } from "./types";
|
||||
|
||||
export const boards: BoardDef[] = [
|
||||
{
|
||||
id: "lilygo_tbeam",
|
||||
name: "LilyGo T-Beam (classic)",
|
||||
mcu: "ESP32",
|
||||
radio: "SX1276",
|
||||
family: "esp32",
|
||||
flashMethod: "web-serial",
|
||||
assetPattern: "donglora-lilygo_tbeam-v{version}.bin",
|
||||
notes:
|
||||
"Original T-Beam with ESP32 + SX1276 + SSD1306 OLED + AXP192 PMIC. Flashes over the on-board CP2102 USB-to-UART bridge. Clients connect via serial (BLE planned).",
|
||||
},
|
||||
{
|
||||
id: "elecrow_thinknode_m2",
|
||||
name: "ELECROW ThinkNode-M2",
|
||||
mcu: "ESP32-S3",
|
||||
radio: "SX1262",
|
||||
family: "esp32s3",
|
||||
flashMethod: "download-only",
|
||||
flashMethod: "web-serial",
|
||||
assetPattern: "donglora-elecrow_thinknode_m2-v{version}.bin",
|
||||
notes:
|
||||
"Elecrow Meshtastic handset with SH1106 OLED. Uses CP2102 USB-to-UART bridge — download the .bin and flash with espflash CLI.",
|
||||
"Elecrow Meshtastic handset with SH1106 OLED. Flashes over the built-in CP2102 USB-to-UART bridge — no native USB, so clients connect via serial (BLE planned).",
|
||||
},
|
||||
{
|
||||
id: "heltec_v3_uart",
|
||||
name: "Heltec V3 (UART) — stock",
|
||||
mcu: "ESP32-S3",
|
||||
radio: "SX1262",
|
||||
family: "esp32s3",
|
||||
flashMethod: "web-serial",
|
||||
assetPattern: "donglora-heltec_v3_uart-v{version}.bin",
|
||||
notes:
|
||||
"Default Heltec V3 hardware — pick this one if you bought a stock V3. Flashes over the board's CP2102 USB-to-UART bridge. Clients connect via serial (BLE planned), not native USB.",
|
||||
},
|
||||
{
|
||||
id: "heltec_v3",
|
||||
name: "Heltec V3",
|
||||
name: "Heltec V3 (USB CDC) — modded",
|
||||
mcu: "ESP32-S3",
|
||||
radio: "SX1262",
|
||||
family: "esp32s3",
|
||||
flashMethod: "web-serial",
|
||||
assetPattern: "donglora-heltec_v3-v{version}.bin",
|
||||
notes: "Native USB CDC-ACM (hardware mod may be required on some units)",
|
||||
notes:
|
||||
"Only choose this if you've hardware-modded your V3 for native USB CDC-ACM. Most stock V3s should use the UART variant above.",
|
||||
},
|
||||
{
|
||||
id: "heltec_v4",
|
||||
@@ -32,24 +55,37 @@ export const boards: BoardDef[] = [
|
||||
assetPattern: "donglora-heltec_v4-v{version}.bin",
|
||||
},
|
||||
{
|
||||
id: "heltec_v3_uart",
|
||||
name: "Heltec V3 (UART)",
|
||||
id: "heltec_mesh_node_t114",
|
||||
name: "Heltec Mesh Node T114",
|
||||
mcu: "nRF52840",
|
||||
radio: "SX1262",
|
||||
family: "nrf52840",
|
||||
flashMethod: "uf2",
|
||||
assetPattern: "donglora-heltec_mesh_node_t114-v{version}.uf2",
|
||||
notes:
|
||||
"nRF52840 + SX1262 with a 1.14\" 240×135 ST7789 color TFT — the first donglora board with a color display. Flashes over native USB CDC-ACM via the Adafruit nRF52 UF2 bootloader — double-tap RESET to expose the drive.",
|
||||
},
|
||||
{
|
||||
id: "lilygo_tbeam_supreme",
|
||||
name: "LilyGo T-Beam S3 Supreme",
|
||||
mcu: "ESP32-S3",
|
||||
radio: "SX1262",
|
||||
family: "esp32s3",
|
||||
flashMethod: "download-only",
|
||||
assetPattern: "donglora-heltec_v3_uart-v{version}.bin",
|
||||
flashMethod: "web-serial",
|
||||
assetPattern: "donglora-lilygo_tbeam_supreme-v{version}.bin",
|
||||
notes:
|
||||
"Uses CP2102 USB-to-UART bridge. Web flashing is unreliable — download the .bin and flash with espflash CLI.",
|
||||
"T-Beam Supreme with ESP32-S3 + SX1262 + SH1106 OLED + AXP2101 PMIC. Flashes over native USB CDC-ACM — no UART bridge.",
|
||||
},
|
||||
{
|
||||
id: "rak_wisblock_4631",
|
||||
name: "RAK WisBlock 4631",
|
||||
name: "RAK4631 WisBlock",
|
||||
mcu: "nRF52840",
|
||||
radio: "SX1262",
|
||||
family: "nrf52840",
|
||||
flashMethod: "uf2",
|
||||
assetPattern: "donglora-rak_wisblock_4631-v{version}.uf2",
|
||||
notes:
|
||||
"RAK4631 Core (nRF52840 + SX1262) on a RAK19007 base, optionally with the RAK1921 OLED. Flashes over native USB CDC-ACM via the Adafruit nRF52 UF2 bootloader — double-tap RESET to expose the RAK4631 drive.",
|
||||
},
|
||||
{
|
||||
id: "wio_tracker_l1",
|
||||
@@ -75,6 +111,28 @@ export function getAssetName(board: BoardDef, version: string): string {
|
||||
return board.assetPattern.replace("{version}", version);
|
||||
}
|
||||
|
||||
export function badgeText(board: BoardDef): string {
|
||||
switch (board.flashMethod) {
|
||||
case "web-serial":
|
||||
return "Web Flash";
|
||||
case "uf2":
|
||||
return "UF2 Download";
|
||||
case "download-only":
|
||||
return "CLI Only";
|
||||
}
|
||||
}
|
||||
|
||||
export function badgeColor(board: BoardDef): string {
|
||||
switch (board.flashMethod) {
|
||||
case "web-serial":
|
||||
return "text-accent";
|
||||
case "uf2":
|
||||
return "text-info";
|
||||
case "download-only":
|
||||
return "text-warn";
|
||||
}
|
||||
}
|
||||
|
||||
import type { ReleaseAsset } from "./types";
|
||||
|
||||
export interface AssetUrls {
|
||||
|
||||
+20
-4
@@ -1,5 +1,7 @@
|
||||
import type { FlashProgress } from "./types";
|
||||
|
||||
export type EspChipFamily = "esp32" | "esp32s3";
|
||||
|
||||
export function isWebSerialSupported(): boolean {
|
||||
return typeof navigator !== "undefined" && "serial" in navigator;
|
||||
}
|
||||
@@ -19,7 +21,20 @@ async function loadEsptool() {
|
||||
return { ESPLoader: mod.ESPLoader, Transport: mod.Transport };
|
||||
}
|
||||
|
||||
export async function connect(callbacks: FlashCallbacks): Promise<FlashSession> {
|
||||
function chipFamilyLabel(family: EspChipFamily): string {
|
||||
return family === "esp32s3" ? "ESP32-S3" : "ESP32";
|
||||
}
|
||||
|
||||
function isChipCompatible(detected: string, expected: EspChipFamily): boolean {
|
||||
if (expected === "esp32s3") return detected.includes("ESP32-S3");
|
||||
// Classic ESP32: esptool-js reports "ESP32". Exclude S2/S3/C3/C6/H2/P4 variants.
|
||||
return /^ESP32(?![-\s]?(S|C|H|P)\d)/i.test(detected);
|
||||
}
|
||||
|
||||
export async function connect(
|
||||
callbacks: FlashCallbacks,
|
||||
expectedChip: EspChipFamily,
|
||||
): Promise<FlashSession> {
|
||||
if (!isWebSerialSupported()) {
|
||||
throw new Error("Web Serial API is not supported in this browser. Use Chrome, Edge, or Opera.");
|
||||
}
|
||||
@@ -45,16 +60,17 @@ export async function connect(callbacks: FlashCallbacks): Promise<FlashSession>
|
||||
},
|
||||
});
|
||||
|
||||
callbacks.onLog("Connecting to ESP32...");
|
||||
const expectedLabel = chipFamilyLabel(expectedChip);
|
||||
callbacks.onLog(`Connecting to ${expectedLabel}...`);
|
||||
callbacks.onProgress({ phase: "Connecting", percent: 0, message: "Detecting chip..." });
|
||||
await loader.main();
|
||||
|
||||
const chip = loader.chip.CHIP_NAME;
|
||||
callbacks.onLog(`Detected: ${chip}`);
|
||||
|
||||
if (!chip.includes("ESP32-S3")) {
|
||||
if (!isChipCompatible(chip, expectedChip)) {
|
||||
await transport.disconnect();
|
||||
throw new Error(`Expected ESP32-S3 but detected ${chip}. Make sure you selected the correct board.`);
|
||||
throw new Error(`Expected ${expectedLabel} but detected ${chip}. Make sure you selected the correct board.`);
|
||||
}
|
||||
|
||||
return { transport, loader };
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export type BoardFamily = "esp32s3" | "nrf52840" | "rp2040";
|
||||
export type BoardFamily = "esp32" | "esp32s3" | "nrf52840" | "rp2040";
|
||||
export type FlashMethod = "web-serial" | "uf2" | "download-only";
|
||||
|
||||
export interface BoardDef {
|
||||
|
||||
@@ -6,7 +6,7 @@ import Flasher from "../components/Flasher.svelte";
|
||||
<Base title="DongLoRa Flasher" description="Flash DongLoRa firmware onto your LoRa board from the browser">
|
||||
<h1 class="font-mono text-3xl font-bold mb-2">Firmware Flasher</h1>
|
||||
<p class="text-text-muted mb-8">
|
||||
Flash DongLoRa firmware directly from your browser using Web Serial, or download UF2 files for nRF boards.
|
||||
Flash DongLoRa firmware directly from your browser using Web Serial, or download UF2 files for nRF52840 and RP2040 boards.
|
||||
</p>
|
||||
<Flasher client:load />
|
||||
</Base>
|
||||
+27
-38
@@ -1,6 +1,11 @@
|
||||
---
|
||||
import Base from "../layouts/Base.astro";
|
||||
import { Code } from "astro:components";
|
||||
import { boards, badgeText, badgeColor } from "../lib/boards";
|
||||
|
||||
const sortedBoards = [...boards].sort((a, b) =>
|
||||
a.name.localeCompare(b.name, "en", { sensitivity: "base" }),
|
||||
);
|
||||
|
||||
const quickStartCode = `import donglora as dl
|
||||
|
||||
@@ -224,36 +229,20 @@ your code ──► client lib ──┘</pre>
|
||||
<section class="py-12">
|
||||
<h2 class="font-mono text-2xl font-bold mb-6 text-text">Supported Boards</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="bg-bg-card border border-border p-5 rounded">
|
||||
<h3 class="font-mono font-medium text-text mb-2">ELECROW ThinkNode-M2</h3>
|
||||
<p class="text-sm text-text-muted">ESP32-S3 · SX1262</p>
|
||||
<p class="text-xs text-warn mt-2 font-mono">CLI Only</p>
|
||||
</div>
|
||||
<div class="bg-bg-card border border-border p-5 rounded">
|
||||
<h3 class="font-mono font-medium text-text mb-2">Heltec V3</h3>
|
||||
<p class="text-sm text-text-muted">ESP32-S3 · SX1262</p>
|
||||
<p class="text-xs text-accent mt-2 font-mono">Web Flash</p>
|
||||
</div>
|
||||
<div class="bg-bg-card border border-border p-5 rounded">
|
||||
<h3 class="font-mono font-medium text-text mb-2">Heltec V4</h3>
|
||||
<p class="text-sm text-text-muted">ESP32-S3 · SX1262</p>
|
||||
<p class="text-xs text-accent mt-2 font-mono">Web Flash</p>
|
||||
</div>
|
||||
<div class="bg-bg-card border border-border p-5 rounded">
|
||||
<h3 class="font-mono font-medium text-text mb-2">RAK WisBlock 4631</h3>
|
||||
<p class="text-sm text-text-muted">nRF52840 · SX1262</p>
|
||||
<p class="text-xs text-info mt-2 font-mono">UF2 Download</p>
|
||||
</div>
|
||||
<div class="bg-bg-card border border-border p-5 rounded">
|
||||
<h3 class="font-mono font-medium text-text mb-2">Wio Tracker L1</h3>
|
||||
<p class="text-sm text-text-muted">nRF52840 · SX1262</p>
|
||||
<p class="text-xs text-info mt-2 font-mono">UF2 Download</p>
|
||||
</div>
|
||||
<div class="bg-bg-card border border-border p-5 rounded">
|
||||
<h3 class="font-mono font-medium text-text mb-2">Waveshare RP2040-LoRa</h3>
|
||||
<p class="text-sm text-text-muted">RP2040 · SX1262</p>
|
||||
<p class="text-xs text-info mt-2 font-mono">UF2 Download</p>
|
||||
</div>
|
||||
{
|
||||
sortedBoards.map((board) => (
|
||||
<a
|
||||
href={`/flash?board=${board.id}`}
|
||||
class="block bg-bg-card border border-border p-5 rounded transition-colors hover:border-border-bright hover:bg-bg-elevated no-underline"
|
||||
>
|
||||
<h3 class="font-mono font-medium text-text mb-2">{board.name}</h3>
|
||||
<p class="text-sm text-text-muted">
|
||||
{board.mcu} · {board.radio}
|
||||
</p>
|
||||
<p class={`text-xs ${badgeColor(board)} mt-2 font-mono`}>{badgeText(board)}</p>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -268,7 +257,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mb-3">The code that runs on the LoRa board itself. Owns the radio hardware, speaks USB.</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-8">
|
||||
<a
|
||||
href="https://github.com/donglora/firmware"
|
||||
href="https://git.swaits.com/donglora/firmware"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -283,7 +272,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mb-3">Run on your computer. Handle USB discovery, protocol framing, and encoding so you just call <code class="font-mono text-xs text-accent">send()</code> and <code class="font-mono text-xs text-accent">receive()</code>.</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-8">
|
||||
<a
|
||||
href="https://github.com/donglora/client-rs"
|
||||
href="https://git.swaits.com/donglora/client-rs"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -292,7 +281,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mt-1">Rust client library — <code class="font-mono text-xs text-accent">donglora-client</code> on crates.io</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/donglora/client-py"
|
||||
href="https://git.swaits.com/donglora/client-py"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -307,7 +296,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mb-3">Optional daemon that lets multiple programs share a single dongle at the same time.</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-8">
|
||||
<a
|
||||
href="https://github.com/donglora/mux-rs"
|
||||
href="https://git.swaits.com/donglora/mux-rs"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -316,7 +305,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mt-1">USB multiplexer daemon (Rust)</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/donglora/mux-py"
|
||||
href="https://git.swaits.com/donglora/mux-py"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -331,7 +320,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mb-3">Real programs built on the layers above. Examples of what you can do with a DongLoRa dongle.</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<a
|
||||
href="https://github.com/donglora/bridge"
|
||||
href="https://git.swaits.com/donglora/bridge"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -340,7 +329,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mt-1">P2P LoRa bridge — relays encrypted packets across the internet (Rust)</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/donglora/ai-bot"
|
||||
href="https://git.swaits.com/donglora/ai-bot"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -349,7 +338,7 @@ your code ──► client lib ──┘</pre>
|
||||
<p class="text-sm text-text-muted mt-1">MeshCore AI chatbot — mesh name "Orac" (Python)</p>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/donglora/examples"
|
||||
href="https://git.swaits.com/donglora/examples"
|
||||
class="bg-bg-card border border-border p-4 rounded hover:border-border-bright transition-colors block"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -70,4 +70,150 @@
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Changelog typography (markdown rendered from GitHub release bodies).
|
||||
Kept here rather than in VersionSelector.svelte because the Astro +
|
||||
Svelte 5 build pipeline wasn't emitting the component's <style>
|
||||
block into any CSS file, leaving the prod changelog unstyled. */
|
||||
.changelog {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* Top-level heading (e.g. `## [0.4.0] - 2026-04-11`) */
|
||||
.changelog h1,
|
||||
.changelog h2 {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.changelog h1:first-child,
|
||||
.changelog h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Section heading (e.g. `### Added`) */
|
||||
.changelog h3 {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text);
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.changelog h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Color-code Keep-a-Changelog sections */
|
||||
.changelog h3[data-section="added"] { color: var(--color-accent); }
|
||||
.changelog h3[data-section="changed"] { color: var(--color-warn); }
|
||||
.changelog h3[data-section="fixed"] { color: var(--color-info); }
|
||||
.changelog h3[data-section="removed"],
|
||||
.changelog h3[data-section="deprecated"] { color: var(--color-error); }
|
||||
.changelog h3[data-section="security"] { color: var(--color-warn); }
|
||||
|
||||
.changelog h4,
|
||||
.changelog h5,
|
||||
.changelog h6 {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.changelog p {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
.changelog ul,
|
||||
.changelog ol {
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 1.4rem;
|
||||
}
|
||||
.changelog ul { list-style-type: disc; }
|
||||
.changelog ol { list-style-type: decimal; }
|
||||
.changelog li { margin-bottom: 0.35rem; }
|
||||
.changelog li::marker { color: var(--color-text-dim); }
|
||||
.changelog li > p { margin: 0; }
|
||||
/* Nested lists: different marker, tighter spacing */
|
||||
.changelog ul ul {
|
||||
list-style-type: circle;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.changelog ul ul ul { list-style-type: square; }
|
||||
|
||||
/* Inline code */
|
||||
.changelog code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85em;
|
||||
color: var(--color-accent);
|
||||
background: var(--color-bg-elevated);
|
||||
padding: 0.1em 0.35em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Fenced code blocks */
|
||||
.changelog pre {
|
||||
background: var(--color-bg-elevated);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.75rem 0;
|
||||
font-size: 0.8125rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.changelog pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
color: var(--color-text);
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.changelog a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.changelog a:hover {
|
||||
color: var(--color-accent-bright);
|
||||
text-decoration-color: var(--color-accent-bright);
|
||||
}
|
||||
|
||||
/* Horizontal rule (version separator) */
|
||||
.changelog hr {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.changelog strong {
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.changelog em {
|
||||
color: var(--color-text);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.changelog blockquote {
|
||||
border-left: 3px solid var(--color-border-bright);
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.25rem 0 0.25rem 1rem;
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user