Loading CONTRIBUTING.md +11 −0 Changes for CONTRIBUTING.md: 11 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -34,6 +34,17 @@ matching the actual UI labels. English and Chinese copy must retain the same meaning and degree of formality. Update existing translation entries rather than adding duplicate messages or components. ## Panel Visual Style Reuse `PageHeader`, the `Card` family, and the shared form and button components. Card titles, borders, corner radii, spacing, and save footers follow their shared definitions; avoid redefining these styles in individual pages. Use white card backgrounds in the light theme, restrained semantic colors, and 16px functional icons. Use `CardFooter` for save actions and `Button` for interactive controls. Tables, consoles, compact statistics, and status messages may retain spacing and colors suited to their content. Preserve the shared page margins and bottom spacing. ## Local Development You can do most day-to-day development on macOS or Linux without a full cluster. Loading panel/src/components/DistributedNodes.tsx +1 −1 Changes for panel/src/components/DistributedNodes.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ export function DistributedNodes() { usePolling(nodes.reload, 10_000); return ( <Card> <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle>{t("nodes")}</CardTitle><Button variant="outline" size="sm" disabled={nodes.loading} onClick={nodes.reload}><RefreshCw />{t("nodes_reload")}</Button></CardHeader> <CardHeader className="flex-row flex-wrap items-center justify-between gap-3"><CardTitle>{t("nodes")}</CardTitle><Button variant="outline" size="sm" disabled={nodes.loading} onClick={nodes.reload}><RefreshCw />{t("nodes_reload")}</Button></CardHeader> <CardContent className="space-y-3"> {nodes.loading && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("nodes_loading")}</p>} {!nodes.loading && !nodes.error && nodes.data?.length === 0 && <p className="text-sm text-muted-foreground">{t("nodes_empty")}</p>} Loading panel/src/components/EditServerDialog.tsx +1 −1 Changes for panel/src/components/EditServerDialog.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -214,7 +214,7 @@ export function EditServerDialog({ {/* Unified Sidebar Link/Card style trigger */} <button type="button" className="group flex w-full text-left items-center gap-3 rounded-lg border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-accent focus:outline-none" className="group flex w-full text-left items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-accent focus:outline-none" > <Settings className="h-5 w-5 shrink-0 text-primary" /> <div className="min-w-0 flex-1 text-left"> Loading panel/src/components/NodeControlPanel.tsx +1 −1 Changes for panel/src/components/NodeControlPanel.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -52,7 +52,7 @@ export function NodeControlPanel({ distributed }: { distributed: boolean | null const showForm = distributed === true || distributed === false && enabling; const valid = confirmed && (action === "enable" || !!name.trim() && !!ssh.trim()) && (action === "approve" || !!ip.trim()); return <Card> <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle>{t("node_control_title")}</CardTitle><Button variant="outline" size="sm" disabled={tasks.loading} onClick={() => { tasks.reload(); progress.reload(); }}><RefreshCw />{t("node_control_reload")}</Button></CardHeader> <CardHeader className="flex-row flex-wrap items-center justify-between gap-3"><CardTitle>{t("node_control_title")}</CardTitle><Button variant="outline" size="sm" disabled={tasks.loading} onClick={() => { tasks.reload(); progress.reload(); }}><RefreshCw />{t("node_control_reload")}</Button></CardHeader> <CardContent className="space-y-5"> <p className="text-sm leading-relaxed text-muted-foreground">{t("node_control_description")}</p> {tasks.loading && !tasks.data && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("node_control_loading")}</p>} Loading panel/src/components/PlayerEntrySettings.tsx +4 −4 Changes for panel/src/components/PlayerEntrySettings.tsx: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { ArrowRight, DoorOpen, Globe, Loader2, LogIn, RefreshCw, Route, Save, Server, ShieldCheck } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; Loading Loading @@ -60,7 +60,7 @@ export function PlayerEntrySettings() { } finally { setSaving(false); } } return <Card> <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle className="flex items-center gap-2"><Route className="h-4 w-4 text-primary" aria-hidden="true" />{t("entry_title")}</CardTitle><Button variant="outline" size="sm" disabled={dirty || saving || query.loading} onClick={() => { query.reload(); fleet.reload(); }}><RefreshCw />{t("platform_reload")}</Button></CardHeader> <CardHeader className="flex-row flex-wrap items-center justify-between gap-3"><CardTitle className="flex items-center gap-2"><Route className="h-4 w-4 text-primary" aria-hidden="true" />{t("entry_title")}</CardTitle><Button variant="outline" size="sm" disabled={dirty || saving || query.loading} onClick={() => { query.reload(); fleet.reload(); }}><RefreshCw />{t("platform_reload")}</Button></CardHeader> <CardContent className="space-y-6" aria-busy={query.loading}> <p className="text-sm text-muted-foreground">{t("entry_description")}</p> {query.loading && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("platform_loading")}</p>} Loading Loading @@ -90,8 +90,8 @@ export function PlayerEntrySettings() { {query.error != null && <MessageLine kind="error" message={humanizeError(query.error)} />} {fleet.error != null && <MessageLine kind="error" message={humanizeError(fleet.error)} />} {message && <MessageLine kind={message.kind} message={message.text} />} <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4"><p className="text-xs text-muted-foreground">{t("entry_scope")}</p><div className="flex gap-2">{dirty && <Button variant="outline" disabled={saving} onClick={() => { if (saved) setDraft(saved); setMessage(null); }}>{t("platform_discard")}</Button>}<Button disabled={busy || !dirty || !valid} onClick={() => void save()}>{saving ? <Loader2 className="animate-spin" /> : <Save />}{t("entry_save")}</Button></div></div> {reauth.dialog} </CardContent> <CardFooter><p className="text-xs text-muted-foreground">{t("entry_scope")}</p><div className="flex gap-2">{dirty && <Button variant="outline" disabled={saving} onClick={() => { if (saved) setDraft(saved); setMessage(null); }}>{t("platform_discard")}</Button>}<Button disabled={busy || !dirty || !valid} onClick={() => void save()}>{saving ? <Loader2 className="animate-spin" /> : <Save />}{t("entry_save")}</Button></div></CardFooter> {reauth.dialog} </Card>; } Loading
CONTRIBUTING.md +11 −0 Changes for CONTRIBUTING.md: 11 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -34,6 +34,17 @@ matching the actual UI labels. English and Chinese copy must retain the same meaning and degree of formality. Update existing translation entries rather than adding duplicate messages or components. ## Panel Visual Style Reuse `PageHeader`, the `Card` family, and the shared form and button components. Card titles, borders, corner radii, spacing, and save footers follow their shared definitions; avoid redefining these styles in individual pages. Use white card backgrounds in the light theme, restrained semantic colors, and 16px functional icons. Use `CardFooter` for save actions and `Button` for interactive controls. Tables, consoles, compact statistics, and status messages may retain spacing and colors suited to their content. Preserve the shared page margins and bottom spacing. ## Local Development You can do most day-to-day development on macOS or Linux without a full cluster. Loading
panel/src/components/DistributedNodes.tsx +1 −1 Changes for panel/src/components/DistributedNodes.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -17,7 +17,7 @@ export function DistributedNodes() { usePolling(nodes.reload, 10_000); return ( <Card> <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle>{t("nodes")}</CardTitle><Button variant="outline" size="sm" disabled={nodes.loading} onClick={nodes.reload}><RefreshCw />{t("nodes_reload")}</Button></CardHeader> <CardHeader className="flex-row flex-wrap items-center justify-between gap-3"><CardTitle>{t("nodes")}</CardTitle><Button variant="outline" size="sm" disabled={nodes.loading} onClick={nodes.reload}><RefreshCw />{t("nodes_reload")}</Button></CardHeader> <CardContent className="space-y-3"> {nodes.loading && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("nodes_loading")}</p>} {!nodes.loading && !nodes.error && nodes.data?.length === 0 && <p className="text-sm text-muted-foreground">{t("nodes_empty")}</p>} Loading
panel/src/components/EditServerDialog.tsx +1 −1 Changes for panel/src/components/EditServerDialog.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -214,7 +214,7 @@ export function EditServerDialog({ {/* Unified Sidebar Link/Card style trigger */} <button type="button" className="group flex w-full text-left items-center gap-3 rounded-lg border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-accent focus:outline-none" className="group flex w-full text-left items-center gap-3 rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-accent focus:outline-none" > <Settings className="h-5 w-5 shrink-0 text-primary" /> <div className="min-w-0 flex-1 text-left"> Loading
panel/src/components/NodeControlPanel.tsx +1 −1 Changes for panel/src/components/NodeControlPanel.tsx: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -52,7 +52,7 @@ export function NodeControlPanel({ distributed }: { distributed: boolean | null const showForm = distributed === true || distributed === false && enabling; const valid = confirmed && (action === "enable" || !!name.trim() && !!ssh.trim()) && (action === "approve" || !!ip.trim()); return <Card> <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle>{t("node_control_title")}</CardTitle><Button variant="outline" size="sm" disabled={tasks.loading} onClick={() => { tasks.reload(); progress.reload(); }}><RefreshCw />{t("node_control_reload")}</Button></CardHeader> <CardHeader className="flex-row flex-wrap items-center justify-between gap-3"><CardTitle>{t("node_control_title")}</CardTitle><Button variant="outline" size="sm" disabled={tasks.loading} onClick={() => { tasks.reload(); progress.reload(); }}><RefreshCw />{t("node_control_reload")}</Button></CardHeader> <CardContent className="space-y-5"> <p className="text-sm leading-relaxed text-muted-foreground">{t("node_control_description")}</p> {tasks.loading && !tasks.data && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("node_control_loading")}</p>} Loading
panel/src/components/PlayerEntrySettings.tsx +4 −4 Changes for panel/src/components/PlayerEntrySettings.tsx: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { ArrowRight, DoorOpen, Globe, Loader2, LogIn, RefreshCw, Route, Save, Server, ShieldCheck } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; Loading Loading @@ -60,7 +60,7 @@ export function PlayerEntrySettings() { } finally { setSaving(false); } } return <Card> <CardHeader className="flex-row items-center justify-between gap-3"><CardTitle className="flex items-center gap-2"><Route className="h-4 w-4 text-primary" aria-hidden="true" />{t("entry_title")}</CardTitle><Button variant="outline" size="sm" disabled={dirty || saving || query.loading} onClick={() => { query.reload(); fleet.reload(); }}><RefreshCw />{t("platform_reload")}</Button></CardHeader> <CardHeader className="flex-row flex-wrap items-center justify-between gap-3"><CardTitle className="flex items-center gap-2"><Route className="h-4 w-4 text-primary" aria-hidden="true" />{t("entry_title")}</CardTitle><Button variant="outline" size="sm" disabled={dirty || saving || query.loading} onClick={() => { query.reload(); fleet.reload(); }}><RefreshCw />{t("platform_reload")}</Button></CardHeader> <CardContent className="space-y-6" aria-busy={query.loading}> <p className="text-sm text-muted-foreground">{t("entry_description")}</p> {query.loading && <p role="status" className="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 className="h-4 w-4 animate-spin" />{t("platform_loading")}</p>} Loading Loading @@ -90,8 +90,8 @@ export function PlayerEntrySettings() { {query.error != null && <MessageLine kind="error" message={humanizeError(query.error)} />} {fleet.error != null && <MessageLine kind="error" message={humanizeError(fleet.error)} />} {message && <MessageLine kind={message.kind} message={message.text} />} <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4"><p className="text-xs text-muted-foreground">{t("entry_scope")}</p><div className="flex gap-2">{dirty && <Button variant="outline" disabled={saving} onClick={() => { if (saved) setDraft(saved); setMessage(null); }}>{t("platform_discard")}</Button>}<Button disabled={busy || !dirty || !valid} onClick={() => void save()}>{saving ? <Loader2 className="animate-spin" /> : <Save />}{t("entry_save")}</Button></div></div> {reauth.dialog} </CardContent> <CardFooter><p className="text-xs text-muted-foreground">{t("entry_scope")}</p><div className="flex gap-2">{dirty && <Button variant="outline" disabled={saving} onClick={() => { if (saved) setDraft(saved); setMessage(null); }}>{t("platform_discard")}</Button>}<Button disabled={busy || !dirty || !valid} onClick={() => void save()}>{saving ? <Loader2 className="animate-spin" /> : <Save />}{t("entry_save")}</Button></div></CardFooter> {reauth.dialog} </Card>; }