Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ dist
dist-ssr
*.local

# release artefacts — produced by scripts/package-theme.mjs and
# .github/workflows/release.yml, never hand-edited
theme.tar.gz
theme.tar.gz.sha256

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ npm run dev

将目录复制到 hub 的 `--themes` 位置,在后台「主题」页切换,无需重启。

本地构建并打包:

```bash
npm run build && npm run package-theme
```

产出 `theme.tar.gz` 与 `theme.tar.gz.sha256`,布局与 tag release 里的产物一致。

## 主题契约

主题是纯静态 SPA,只能依赖下列同源接口:
Expand Down
74 changes: 74 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "tsc -b && vite build",
"lint": "oxlint",
"test": "node src/lib/format.test.ts && node src/lib/api.test.ts",
"preview": "vite preview"
"preview": "vite preview",
"package-theme": "node scripts/package-theme.mjs"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
Expand All @@ -31,6 +32,7 @@
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.1.0",
"oxlint": "^1.79.0",
"tar": "^7.4.3",
"typescript": "~6.0.2",
"vite": "^8.2.2"
}
Expand Down
53 changes: 53 additions & 0 deletions scripts/package-theme.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Package the build output as an installable theme bundle. The layout
// (dist/ + theme.json + optional preview.png) matches what release.yml
// archives and what the hub expects under <themes>/<short>/.
import { createHash } from "node:crypto"
import { existsSync, readFileSync, writeFileSync } from "node:fs"
import { resolve } from "node:path"
import { create } from "tar"

const root = import.meta.dirname + "/.."
const tarball = resolve(root, "theme.tar.gz")

// Check the build artefact, not the directory: dist/index.html exists means
// vite build ran. Pass the whole `dist/` directory to tar so it recurses into
// dist/assets/ (the actual JS/CSS chunks). Passing a single file would only
// archive that one literal path.
const required = ["dist/index.html", "theme.json"]
const optional = ["preview.png"]

const missing = required.filter((p) => !existsSync(resolve(root, p)))
if (missing.length) {
console.error(`缺少构建产物:${missing.join("、")}\n请先 npm run build`)
process.exit(1)
}

const entries = [
"dist",
"theme.json",
...optional.filter((p) => existsSync(resolve(root, p))),
]

// portable: drop uid/gid/mtime so the same source produces the same archive
// regardless of which machine ran it -- mirrors what release.yml asks tar for.
// Drop Finder/Win explorer metadata so local archives match what CI emits.
await create(
{
gzip: true,
file: tarball,
cwd: root,
portable: true,
filter: (path) => {
const base = path.split("/").pop()
return base !== ".DS_Store" && base !== "Thumbs.db" && base !== "desktop.ini"
},
},
entries,
)

const bytes = readFileSync(tarball)
const sha256 = createHash("sha256").update(bytes).digest("hex")
writeFileSync(`${tarball}.sha256`, `${sha256} theme.tar.gz\n`)

console.log(`✓ theme.tar.gz (${(bytes.length / 1024).toFixed(1)} KB)`)
console.log(`✓ sha256 ${sha256.slice(0, 12)}… → theme.tar.gz.sha256`)
2 changes: 1 addition & 1 deletion src/components/NodeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function Status({ node }: { node: Node }) {
variant="outline"
className={cn("tnum shrink-0 gap-1.5 font-normal", !node.online && "text-muted-foreground")}
>
<span className={cn("size-1.5 rounded-full", node.online ? "bg-foreground" : "bg-muted-foreground/40")} />
<span className={cn("size-1.5 rounded-full", node.online ? "bg-online" : "bg-muted-foreground/40")} />
{label.trim()}
</Badge>
)
Expand Down
6 changes: 6 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
darkens as it fills. */
--ok: oklch(0.62 0 0);
--warn: oklch(0.44 0 0);
/* Online status breaks the greyscale rule on purpose: the dot must read as
"live" at a glance, distinct from both offline and never-connected. */
--online: oklch(0.72 0.19 142);
}

.dark {
Expand Down Expand Up @@ -70,6 +73,8 @@
/* Inverted against the dark ground: the fuller the bar, the brighter. */
--ok: oklch(0.62 0 0);
--warn: oklch(0.79 0 0);
/* Dark ground eats saturation, so nudge the chroma up to keep "live" legible. */
--online: oklch(0.78 0.21 142);
}

@theme inline {
Expand Down Expand Up @@ -102,6 +107,7 @@
--color-chart-5: var(--chart-5);
--color-ok: var(--ok);
--color-warn: var(--warn);
--color-online: var(--online);
}

@layer base {
Expand Down