diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b9dbd95..a192df8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/panel/src/components/DistributedNodes.tsx b/panel/src/components/DistributedNodes.tsx index 7a82819..f45c532 100644 --- a/panel/src/components/DistributedNodes.tsx +++ b/panel/src/components/DistributedNodes.tsx @@ -17,7 +17,7 @@ export function DistributedNodes() { usePolling(nodes.reload, 10_000); return ( - {t("nodes")} + {t("nodes")} {nodes.loading &&

{t("nodes_loading")}

} {!nodes.loading && !nodes.error && nodes.data?.length === 0 &&

{t("nodes_empty")}

} diff --git a/panel/src/components/EditServerDialog.tsx b/panel/src/components/EditServerDialog.tsx index 40839fb..07eb67c 100644 --- a/panel/src/components/EditServerDialog.tsx +++ b/panel/src/components/EditServerDialog.tsx @@ -214,7 +214,7 @@ export function EditServerDialog({ {/* Unified Sidebar Link/Card style trigger */} + {t("node_control_title")}

{t("node_control_description")}

{tasks.loading && !tasks.data &&

{t("node_control_loading")}

} diff --git a/panel/src/components/PlayerEntrySettings.tsx b/panel/src/components/PlayerEntrySettings.tsx index bfdbd29..7932238 100644 --- a/panel/src/components/PlayerEntrySettings.tsx +++ b/panel/src/components/PlayerEntrySettings.tsx @@ -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"; @@ -60,7 +60,7 @@ export function PlayerEntrySettings() { } finally { setSaving(false); } } return - +

{t("entry_description")}

{query.loading &&

{t("platform_loading")}

} @@ -90,8 +90,8 @@ export function PlayerEntrySettings() { {query.error != null && } {fleet.error != null && } {message && } -

{t("entry_scope")}

{dirty && }
- {reauth.dialog}
+

{t("entry_scope")}

{dirty && }
+ {reauth.dialog}
; } diff --git a/panel/src/components/ui/button.tsx b/panel/src/components/ui/button.tsx index 4feec6c..9844876 100644 --- a/panel/src/components/ui/button.tsx +++ b/panel/src/components/ui/button.tsx @@ -4,16 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 active:scale-[0.97]", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { default: - "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 hover:shadow", + "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 hover:shadow", + "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline bg-transparent", }, diff --git a/panel/src/components/ui/card.tsx b/panel/src/components/ui/card.tsx index 886d396..d26b299 100644 --- a/panel/src/components/ui/card.tsx +++ b/panel/src/components/ui/card.tsx @@ -8,7 +8,7 @@ export const Card = React.forwardRef<
>(({ className, ...props }, ref) => ( -
+
)); CardHeader.displayName = "CardHeader"; @@ -30,7 +30,7 @@ export const CardTitle = React.forwardRef< >(({ className, ...props }, ref) => (

)); @@ -40,7 +40,7 @@ export const CardContent = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
)); CardContent.displayName = "CardContent"; @@ -48,6 +48,6 @@ export const CardFooter = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -
+
)); CardFooter.displayName = "CardFooter"; diff --git a/panel/src/components/ui/input.tsx b/panel/src/components/ui/input.tsx index a2855bd..a48e13f 100644 --- a/panel/src/components/ui/input.tsx +++ b/panel/src/components/ui/input.tsx @@ -9,7 +9,7 @@ export const Input = React.forwardRef< type={type} ref={ref} className={cn( - "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", + "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", className, )} {...props} diff --git a/panel/src/components/ui/select.tsx b/panel/src/components/ui/select.tsx index 3265534..976b266 100644 --- a/panel/src/components/ui/select.tsx +++ b/panel/src/components/ui/select.tsx @@ -14,7 +14,7 @@ export const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} diff --git a/panel/src/pages/Account.tsx b/panel/src/pages/Account.tsx index 3c18c87..4d68ac8 100644 --- a/panel/src/pages/Account.tsx +++ b/panel/src/pages/Account.tsx @@ -300,7 +300,7 @@ export function Account() { - + {t("minecraft_link")} @@ -328,7 +328,7 @@ export function Account() { - + {t("email_verification")} @@ -423,8 +423,8 @@ export function Account() { - - + + {t("passkeys")} { @@ -817,7 +817,7 @@ function MigrationCard({ userId, hasPasskey }: { userId?: string; hasPasskey: bo return ( - + {t("migration")} diff --git a/panel/src/pages/AccountSessions.tsx b/panel/src/pages/AccountSessions.tsx index 55cf892..297205b 100644 --- a/panel/src/pages/AccountSessions.tsx +++ b/panel/src/pages/AccountSessions.tsx @@ -91,7 +91,7 @@ export function AccountSessionsCard({ version, staff, onSignOut, signingOut }: P return ( - + {t("sessions_title")} diff --git a/panel/src/pages/Dashboard.tsx b/panel/src/pages/Dashboard.tsx index 8df2e93..486b873 100644 --- a/panel/src/pages/Dashboard.tsx +++ b/panel/src/pages/Dashboard.tsx @@ -289,7 +289,7 @@ function FleetView({
{/* 1. 游戏角色绑定 Banner */} - + @@ -299,10 +299,10 @@ function FleetView({ <> {/* 集群负载与健康卡片 */} - - {t("fleet_load")} + + {t("fleet_load")} - + {/* 玩家负载进度条 */}
@@ -356,12 +356,12 @@ function FleetView({ {/* 平台环境与配置参数卡片 (flex-1 自动拉伸) */} - - + + {t("system_specs")} - +
{/* 镜像总数 */}
@@ -449,12 +449,12 @@ function FleetView({ ) : ( - - + + {t("welcome_title")} - +

{t("welcome_desc")}

@@ -483,8 +483,8 @@ function FleetView({ {/* 右侧:3D星图拓扑监控 (lg:col-span-4 h-full) */} - - {t("fleet")} + + {t("fleet")}
diff --git a/panel/src/pages/MySubmissionsPage.tsx b/panel/src/pages/MySubmissionsPage.tsx index c274cbb..46a5764 100644 --- a/panel/src/pages/MySubmissionsPage.tsx +++ b/panel/src/pages/MySubmissionsPage.tsx @@ -342,67 +342,67 @@ export function MySubmissionsPage() { {/* Filters Bar */}
-
- - - - +
diff --git a/panel/src/pages/ServerBackups.tsx b/panel/src/pages/ServerBackups.tsx index 80b8f13..04ff8b5 100644 --- a/panel/src/pages/ServerBackups.tsx +++ b/panel/src/pages/ServerBackups.tsx @@ -947,8 +947,8 @@ export function ServerBackups() { )} - - + + {t("history_note")} @@ -961,7 +961,7 @@ export function ServerBackups() { ) : ( - +
{t("jobs_title")} diff --git a/panel/src/pages/ServerConsole.tsx b/panel/src/pages/ServerConsole.tsx index af39fb2..73e805d 100644 --- a/panel/src/pages/ServerConsole.tsx +++ b/panel/src/pages/ServerConsole.tsx @@ -316,7 +316,7 @@ export function ServerConsole() { )} {!error && startup && ( -
+

{startupBlocked ? : }{t(`startup_${startup.stage}`)}

{startup.startedAt &&

{t("startup_elapsed", { seconds: Math.max(0, Math.floor((Date.now() - new Date(startup.startedAt).getTime()) / 1000)) })}

} {startup.reason &&

{startup.reason === "Unschedulable" && startup.message?.includes("Insufficient memory") ? t("startup_memory") : [startup.reason, startup.message].filter(Boolean).join(": ")}

} @@ -378,9 +378,9 @@ export function ServerConsole() { {owned && ( - +

{t("luckperms_link_title")}

@@ -395,9 +395,9 @@ export function ServerConsole() { itself owns the ownership + readiness gating. */} - +

{t("players_link_title")}

{t("players_link_desc")}

@@ -410,9 +410,9 @@ export function ServerConsole() { restore in fact needs it stopped, so this doorway shows at every phase). */} - +

{t("backups_link_title")}

{t("backups_link_desc")}

@@ -425,9 +425,9 @@ export function ServerConsole() { and offers the stop action; the doorway shows at every phase. */} - +

{t("files_link_title")}

{t("files_link_desc")}

@@ -440,9 +440,9 @@ export function ServerConsole() { doorway shows at every phase and the page owns its gating. */} - +

{t("schedules_link_title")}

{t("schedules_link_desc")}

@@ -467,7 +467,7 @@ export function ServerConsole() { function HostRecovery({ name }: { name: string }) { const { t } = useTranslation("servers"); if (!/^[a-z0-9][a-z0-9-]{0,31}$/.test(name)) return null; - return
{t("host_recovery")}

{t("host_recovery_hint")}

{`kubectl patch minecraftserver ${name} -n minecraft --type=merge -p '{"spec":{"desiredState":"Stopped"}}'
+  return 
{t("host_recovery")}

{t("host_recovery_hint")}

{`kubectl patch minecraftserver ${name} -n minecraft --type=merge -p '{"spec":{"desiredState":"Stopped"}}'
 kubectl scale statefulset ${name} -n minecraft --replicas=0
 kubectl get pods -n minecraft -l felis.lolicon.best/server=${name},felis.lolicon.best/component=server -o wide`}

{t("host_recovery_verify")}

; } diff --git a/panel/src/pages/ServerLuckPerms.tsx b/panel/src/pages/ServerLuckPerms.tsx index 8f001b7..c86322a 100644 --- a/panel/src/pages/ServerLuckPerms.tsx +++ b/panel/src/pages/ServerLuckPerms.tsx @@ -400,9 +400,9 @@ export function ServerLuckPerms() { {/* Left Directory Sidebar: Single card for Search + Online Players. On a wide screen it fills the column; on a phone it fits its content and the list scrolls inside it, so the player's panel below is on the first screen. */} - - - + + + {t("luckperms_batch_players")} @@ -426,7 +426,7 @@ export function ServerLuckPerms() { {t("access_online_title")}
- + {playersLoading && onlinePlayers.length === 0 ? (
@@ -465,7 +465,7 @@ export function ServerLuckPerms() { {/* Right Main Panel */}
{!selectedPlayer ? ( - +

{t("luckperms_select_player_prompt")} @@ -525,13 +525,13 @@ export function ServerLuckPerms() { )} {/* Groups Management Card */} - - - + + + {t("luckperms_groups_title")} - + {lpLoading && !lpInfo ? (

@@ -635,13 +635,13 @@ export function ServerLuckPerms() { {/* Permissions Management Card */} - - - + + + {t("luckperms_permissions_title")} - + {lpLoading && !lpInfo ? (
@@ -831,10 +831,10 @@ export function ServerLuckPerms() { )} {/* Action History Log */} - - + +
- + {t("luckperms_recent_actions")} @@ -851,7 +851,7 @@ export function ServerLuckPerms() { )}
- + {history.length === 0 ? (

{t("luckperms_no_recent_actions")} diff --git a/panel/src/pages/ServerSchedules.tsx b/panel/src/pages/ServerSchedules.tsx index 83603f3..fe9523f 100644 --- a/panel/src/pages/ServerSchedules.tsx +++ b/panel/src/pages/ServerSchedules.tsx @@ -956,8 +956,8 @@ export function ServerSchedules() { )} - - + +

{t("notes_title")} diff --git a/panel/src/pages/admin/AuthSourcesPage.tsx b/panel/src/pages/admin/AuthSourcesPage.tsx index 9ed2745..09980ea 100644 --- a/panel/src/pages/admin/AuthSourcesPage.tsx +++ b/panel/src/pages/admin/AuthSourcesPage.tsx @@ -5,7 +5,7 @@ import { PageHeader } from "@/components/PageHeader"; import { InlineError, MessageLine } from "@/components/MessageLine"; import { isReauthCancelled, useReauth } from "@/components/ReauthDialog"; 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 { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; @@ -90,8 +90,8 @@ export function AuthSourcesPage() { return

- - + +

{t("builtin_title")}

{t("builtin_hint")}

@@ -114,10 +114,10 @@ export function AuthSourcesPage() { [sources[index], sources[index + offset]] = [sources[index + offset], sources[index]]; edit(sources); }; - return - - - }
-
{reauth.dialog}
; } diff --git a/panel/src/pages/admin/DBBackupCard.tsx b/panel/src/pages/admin/DBBackupCard.tsx index 920dd38..cd9a0c8 100644 --- a/panel/src/pages/admin/DBBackupCard.tsx +++ b/panel/src/pages/admin/DBBackupCard.tsx @@ -142,22 +142,22 @@ export function DBBackupCard() { return ( - +
- +
- + {t("dbbackup_title")} {badge} diff --git a/panel/src/pages/admin/ImageAdmin.tsx b/panel/src/pages/admin/ImageAdmin.tsx index c99494e..7e4f950 100644 --- a/panel/src/pages/admin/ImageAdmin.tsx +++ b/panel/src/pages/admin/ImageAdmin.tsx @@ -207,52 +207,52 @@ export function ImageAdmin() { {/* Filters Bar */}
-
- - - +
diff --git a/panel/src/pages/admin/LobbyPage.tsx b/panel/src/pages/admin/LobbyPage.tsx index 06f93bd..6f48e79 100644 --- a/panel/src/pages/admin/LobbyPage.tsx +++ b/panel/src/pages/admin/LobbyPage.tsx @@ -5,8 +5,9 @@ import { useTranslation } from "react-i18next"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { PageHeader } from "@/components/PageHeader"; import { Switch } from "@/components/ui/switch"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PhaseBadge, shownPhase, startFailure } from "@/components/PhaseBadge"; @@ -98,31 +99,31 @@ function ExperienceSettings({ name, server, onDirtyChange, onChanged }: { name: const fields = group.fields.filter((field) => typeof field.value !== "boolean"); const switches = group.fields.filter((field) => typeof field.value === "boolean"); return ( - - - + + +
- {t(group.key)} + {t(group.key)}

{t(`${group.key}_description`)}

- +
{fields.map((field) => { const value = draft ? draft.values[field.key] ?? field.value : ""; const id = `experience-${field.key}`; return (
- + {field.choices ? ( - + ) : field.multiline ? ( -