mirror of
https://github.com/fccview/cronmaster.git
synced 2026-04-22 23:19:20 -04:00
start replacing all UI with terminal UI
This commit is contained in:
@@ -108,12 +108,12 @@ export const CronJobItem = ({
|
||||
},
|
||||
...(job.logsEnabled
|
||||
? [
|
||||
{
|
||||
label: t("cronjobs.viewLogs"),
|
||||
icon: <FileText className="h-3 w-3" />,
|
||||
onClick: () => onViewLogs(job),
|
||||
},
|
||||
]
|
||||
{
|
||||
label: t("cronjobs.viewLogs"),
|
||||
icon: <FileText className="h-3 w-3" />,
|
||||
onClick: () => onViewLogs(job),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: job.paused
|
||||
@@ -148,22 +148,21 @@ export const CronJobItem = ({
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
className={`glass-card p-4 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors ${
|
||||
isDropdownOpen ? "relative z-10" : ""
|
||||
}`}
|
||||
className={`tui-card p-4 terminal-font transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{(scheduleDisplayMode === "cron" ||
|
||||
scheduleDisplayMode === "both") && (
|
||||
<code className="text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 px-2 py-1 rounded font-mono border border-purple-500/20">
|
||||
{job.schedule}
|
||||
</code>
|
||||
)}
|
||||
<code className="text-sm bg-background0 text-status-warning px-2 py-1 terminal-font ascii-border">
|
||||
{job.schedule}
|
||||
</code>
|
||||
)}
|
||||
{scheduleDisplayMode === "human" && cronExplanation?.isValid && (
|
||||
<div className="flex items-start gap-1.5 border-b border-primary/30 bg-primary/10 rounded text-primary px-2 py-0.5">
|
||||
<Info className="h-3 w-3 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div className="flex items-start gap-1.5 ascii-border bg-background2 px-2 py-0.5">
|
||||
<Info className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm italic">
|
||||
{cronExplanation.humanReadable}
|
||||
</p>
|
||||
@@ -172,7 +171,7 @@ export const CronJobItem = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0 w-full">
|
||||
{commandCopied === job.id && (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
<Check className="h-3 w-3 text-status-success" />
|
||||
)}
|
||||
<pre
|
||||
onClick={(e) => {
|
||||
@@ -181,7 +180,7 @@ export const CronJobItem = ({
|
||||
setCommandCopied(job.id);
|
||||
setTimeout(() => setCommandCopied(null), 3000);
|
||||
}}
|
||||
className="w-full cursor-pointer overflow-x-auto text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border/30 hide-scrollbar"
|
||||
className="w-full cursor-pointer overflow-x-auto text-sm font-medium terminal-font bg-background1 px-2 py-1 ascii-border hide-scrollbar"
|
||||
>
|
||||
{unwrapCommand(displayCommand)}
|
||||
</pre>
|
||||
@@ -191,8 +190,8 @@ export const CronJobItem = ({
|
||||
|
||||
<div className="flex items-center gap-2 pb-2 pt-4">
|
||||
{scheduleDisplayMode === "both" && cronExplanation?.isValid && (
|
||||
<div className="flex items-start gap-1.5 border-b border-primary/30 bg-primary/10 rounded text-primary px-2 py-0.5">
|
||||
<Info className="h-3 w-3 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div className="flex items-start gap-1.5 ascii-border bg-background2 px-2 py-0.5">
|
||||
<Info className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-xs italic">
|
||||
{cronExplanation.humanReadable}
|
||||
</p>
|
||||
@@ -201,7 +200,7 @@ export const CronJobItem = ({
|
||||
|
||||
{job.comment && (
|
||||
<p
|
||||
className="text-xs text-muted-foreground italic truncate"
|
||||
className="text-xs italic truncate"
|
||||
title={job.comment}
|
||||
>
|
||||
{job.comment}
|
||||
@@ -210,13 +209,13 @@ export const CronJobItem = ({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 py-3">
|
||||
<div className="flex items-center gap-1 text-xs bg-muted/50 text-muted-foreground px-2 py-0.5 rounded border border-border/30 cursor-pointer hover:bg-muted/70 transition-colors relative">
|
||||
<div className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border cursor-pointer hover:bg-background2 transition-colors relative terminal-font">
|
||||
<User className="h-3 w-3" />
|
||||
<span>{job.user}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center gap-1 text-xs bg-muted/50 text-muted-foreground px-2 py-0.5 rounded border border-border/30 cursor-pointer hover:bg-muted/70 transition-colors relative"
|
||||
className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border cursor-pointer hover:bg-background2 transition-colors relative terminal-font"
|
||||
title="Click to copy Job UUID"
|
||||
onClick={async () => {
|
||||
const success = await copyToClipboard(job.id);
|
||||
@@ -227,7 +226,7 @@ export const CronJobItem = ({
|
||||
}}
|
||||
>
|
||||
{showCopyConfirmation ? (
|
||||
<Check className="h-3 w-3 text-green-600" />
|
||||
<Check className="h-3 w-3 text-status-success" />
|
||||
) : (
|
||||
<Hash className="h-3 w-3" />
|
||||
)}
|
||||
@@ -235,14 +234,14 @@ export const CronJobItem = ({
|
||||
</div>
|
||||
|
||||
{job.paused && (
|
||||
<span className="text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/20">
|
||||
{t("cronjobs.paused")}
|
||||
<span className="text-xs bg-background2 px-2 py-0.5 ascii-border terminal-font">
|
||||
<span className="text-status-warning">{t("cronjobs.paused")}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{job.logsEnabled && (
|
||||
<span className="text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded border border-blue-500/20">
|
||||
{t("cronjobs.logged")}
|
||||
<span className="text-xs bg-background0 px-2 py-0.5 ascii-border terminal-font">
|
||||
<span className="text-status-info">{t("cronjobs.logged")}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -252,11 +251,11 @@ export const CronJobItem = ({
|
||||
e.stopPropagation();
|
||||
onViewLogs(job);
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs bg-red-500/10 text-red-600 dark:text-red-400 px-2 py-0.5 rounded border border-red-500/30 hover:bg-red-500/20 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border hover:bg-background1 transition-colors cursor-pointer terminal-font"
|
||||
title="Latest execution failed - Click to view error log"
|
||||
>
|
||||
<AlertCircle className="h-3 w-3" />
|
||||
<span>
|
||||
<AlertCircle className="h-3 w-3 text-status-error" />
|
||||
<span className="text-status-error">
|
||||
{t("cronjobs.failed", {
|
||||
exitCode: job.logError?.exitCode?.toString() ?? "",
|
||||
})}
|
||||
@@ -272,12 +271,12 @@ export const CronJobItem = ({
|
||||
e.stopPropagation();
|
||||
onViewLogs(job);
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 px-2 py-0.5 rounded border border-yellow-500/30 hover:bg-yellow-500/20 transition-colors cursor-pointer"
|
||||
className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border hover:bg-background1 transition-colors cursor-pointer terminal-font"
|
||||
title="Latest execution succeeded, but has historical failures - Click to view logs"
|
||||
>
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
<span>{t("cronjobs.healthy")}</span>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
<CheckCircle className="h-3 w-3 text-status-success" />
|
||||
<span className="text-status-warning">{t("cronjobs.healthy")}</span>
|
||||
<AlertTriangle className="h-3 w-3 text-status-warning" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -285,9 +284,9 @@ export const CronJobItem = ({
|
||||
!job.logError?.hasError &&
|
||||
!job.logError?.hasHistoricalFailures &&
|
||||
job.logError?.latestExitCode === 0 && (
|
||||
<div className="flex items-center gap-1 text-xs bg-green-500/10 text-green-600 dark:text-green-400 px-2 py-0.5 rounded border border-green-500/30">
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
<span>{t("cronjobs.healthy")}</span>
|
||||
<div className="flex items-center gap-1 text-xs bg-background0 px-2 py-0.5 ascii-border terminal-font">
|
||||
<CheckCircle className="h-3 w-3 text-status-success" />
|
||||
<span className="text-status-success">{t("cronjobs.healthy")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -139,19 +139,19 @@ export const MinimalCronJobItem = ({
|
||||
return (
|
||||
<div
|
||||
key={job.id}
|
||||
className={`glass-card p-3 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
className={`tui-card p-3 terminal-font transition-colors ${isDropdownOpen ? "relative z-10" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{scheduleDisplayMode === "cron" && (
|
||||
<code className="text-xs bg-purple-500/10 text-purple-600 dark:text-purple-400 px-1.5 py-0.5 rounded font-mono border border-purple-500/20">
|
||||
<code className="text-xs bg-background2 px-1.5 py-0.5 terminal-font ascii-border">
|
||||
{job.schedule}
|
||||
</code>
|
||||
)}
|
||||
{scheduleDisplayMode === "human" && cronExplanation?.isValid && (
|
||||
<div className="flex items-center gap-1 border-b border-primary/30 bg-primary/10 rounded text-primary px-1.5 py-0.5">
|
||||
<Info className="h-3 w-3 text-primary flex-shrink-0" />
|
||||
<div className="flex items-center gap-1 ascii-border bg-background2 px-1.5 py-0.5">
|
||||
<Info className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="text-xs italic truncate max-w-32">
|
||||
{cronExplanation.humanReadable}
|
||||
</span>
|
||||
@@ -159,15 +159,15 @@ export const MinimalCronJobItem = ({
|
||||
)}
|
||||
{scheduleDisplayMode === "both" && (
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="text-xs bg-purple-500/10 text-purple-600 dark:text-purple-400 px-1 py-0.5 rounded font-mono border border-purple-500/20">
|
||||
<code className="text-xs bg-background0 text-status-warning px-1 py-0.5 terminal-font ascii-border">
|
||||
{job.schedule}
|
||||
</code>
|
||||
{cronExplanation?.isValid && (
|
||||
<div
|
||||
className="flex items-center gap-1 border-b border-primary/30 bg-primary/10 rounded text-primary px-1 py-0.5 cursor-help"
|
||||
className="flex items-center gap-1 ascii-border bg-background0 px-1 py-0.5 cursor-help"
|
||||
title={cronExplanation.humanReadable}
|
||||
>
|
||||
<Info className="h-2.5 w-2.5 text-primary flex-shrink-0" />
|
||||
<Info className="h-2.5 w-2.5 flex-shrink-0" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -177,7 +177,7 @@ export const MinimalCronJobItem = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{commandCopied === job.id && (
|
||||
<Check className="h-3 w-3 text-green-600 flex-shrink-0" />
|
||||
<Check className="h-3 w-3 text-status-success flex-shrink-0" />
|
||||
)}
|
||||
<pre
|
||||
onClick={(e) => {
|
||||
@@ -186,7 +186,7 @@ export const MinimalCronJobItem = ({
|
||||
setCommandCopied(job.id);
|
||||
setTimeout(() => setCommandCopied(null), 3000);
|
||||
}}
|
||||
className="flex-1 cursor-pointer overflow-hidden text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border/30 truncate"
|
||||
className="flex-1 cursor-pointer overflow-hidden text-sm font-medium terminal-font bg-background1 px-2 py-1 ascii-border truncate"
|
||||
title={unwrapCommand(job.command)}
|
||||
>
|
||||
{unwrapCommand(displayCommand)}
|
||||
@@ -197,25 +197,25 @@ export const MinimalCronJobItem = ({
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{job.logsEnabled && (
|
||||
<div
|
||||
className="w-2 h-2 bg-blue-500 rounded-full"
|
||||
className="w-2 h-2 bg-status-info ascii-border"
|
||||
title={t("cronjobs.logged")}
|
||||
/>
|
||||
)}
|
||||
{job.paused && (
|
||||
<div
|
||||
className="w-2 h-2 bg-yellow-500 rounded-full"
|
||||
className="w-2 h-2 bg-status-warning ascii-border"
|
||||
title={t("cronjobs.paused")}
|
||||
/>
|
||||
)}
|
||||
{!job.logError?.hasError && job.logsEnabled && (
|
||||
<div
|
||||
className="w-2 h-2 bg-green-500 rounded-full"
|
||||
className="w-2 h-2 bg-status-success ascii-border"
|
||||
title={t("cronjobs.healthy")}
|
||||
/>
|
||||
)}
|
||||
{job.logsEnabled && job.logError?.hasError && (
|
||||
<div
|
||||
className="w-2 h-2 bg-red-500 rounded-full cursor-pointer"
|
||||
className="w-2 h-2 bg-status-error ascii-border cursor-pointer"
|
||||
title="Latest execution failed - Click to view error log"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -225,21 +225,22 @@ export const MinimalCronJobItem = ({
|
||||
)}
|
||||
{!job.logsEnabled && errors.length > 0 && (
|
||||
<div
|
||||
className="w-2 h-2 bg-orange-500 rounded-full cursor-pointer"
|
||||
className="w-2 h-2 bg-status-warning ascii-border cursor-pointer"
|
||||
title={`${errors.length} error(s)`}
|
||||
onClick={(e) => onErrorClick(errors[0])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onRun(job.id)}
|
||||
disabled={runningJobId === job.id || job.paused}
|
||||
className="h-6 w-6 p-0"
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.runCronManually")}
|
||||
aria-label={t("cronjobs.runCronManually")}
|
||||
>
|
||||
{runningJobId === job.id ? (
|
||||
<div className="h-3 w-3 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
@@ -249,7 +250,7 @@ export const MinimalCronJobItem = ({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (job.paused) {
|
||||
@@ -258,8 +259,9 @@ export const MinimalCronJobItem = ({
|
||||
onPause(job.id);
|
||||
}
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
className="btn-outline h-8 px-3"
|
||||
title={t("cronjobs.pauseCronJob")}
|
||||
aria-label={t("cronjobs.pauseCronJob")}
|
||||
>
|
||||
{job.paused ? (
|
||||
<Play className="h-3 w-3" />
|
||||
@@ -269,7 +271,7 @@ export const MinimalCronJobItem = ({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (job.logsEnabled) {
|
||||
@@ -278,12 +280,17 @@ export const MinimalCronJobItem = ({
|
||||
onToggleLogging(job.id);
|
||||
}
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
className="btn-outline h-8 px-3"
|
||||
title={
|
||||
job.logsEnabled
|
||||
? t("cronjobs.viewLogs")
|
||||
: t("cronjobs.enableLogging")
|
||||
}
|
||||
aria-label={
|
||||
job.logsEnabled
|
||||
? t("cronjobs.viewLogs")
|
||||
: t("cronjobs.enableLogging")
|
||||
}
|
||||
>
|
||||
{job.logsEnabled ? (
|
||||
<FileText className="h-3 w-3" />
|
||||
|
||||
@@ -54,7 +54,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsMobileOpen(!isMobileOpen)}
|
||||
className="fixed bottom-4 right-4 z-50 lg:hidden p-2 bg-background/80 backdrop-blur-md border border-border/50 rounded-lg hover:bg-accent transition-colors"
|
||||
className="fixed bottom-4 right-4 z-50 lg:hidden p-2 bg-background0 ascii-border transition-colors terminal-font"
|
||||
>
|
||||
{isMobileOpen ? (
|
||||
<X className="h-5 w-5" />
|
||||
@@ -65,7 +65,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 bg-black/50 z-20 lg:hidden transition-opacity duration-300",
|
||||
"fixed inset-0 bg-background0 z-20 lg:hidden transition-opacity duration-300",
|
||||
isMobileOpen ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
onClick={() => setIsMobileOpen(false)}
|
||||
@@ -74,7 +74,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-background/95 backdrop-blur-md border-r border-border/50 transition-all duration-300 ease-in-out glass-card",
|
||||
"bg-background0 ascii-border transition-all duration-300 ease-in-out terminal-font",
|
||||
isMobileOpen
|
||||
? "fixed left-0 top-0 h-full w-80 z-30 translate-x-0"
|
||||
: "fixed left-0 top-0 h-full w-80 z-30 -translate-x-full lg:translate-x-0",
|
||||
@@ -92,7 +92,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="absolute -right-3 top-[21.5vh] w-6 h-6 bg-background border border-border rounded-full items-center justify-center hover:bg-accent transition-colors z-40 hidden lg:flex"
|
||||
className="absolute -right-3 top-[21.5vh] w-6 h-6 bg-background0 ascii-border items-center justify-center transition-colors z-40 hidden lg:flex"
|
||||
>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
@@ -101,18 +101,18 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="p-4 border-b border-border/50 bg-background/95 backdrop-blur-md">
|
||||
<div className="p-4 ascii-border !border-t-0 border-l-0 !border-r-0 bg-background0">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3",
|
||||
isCollapsed && "lg:justify-center"
|
||||
)}
|
||||
>
|
||||
<div className="p-2 bg-gradient-to-br from-cyan-500/20 to-blue-500/20 rounded-lg flex-shrink-0">
|
||||
<Server className="h-4 w-4 text-cyan-500" />
|
||||
<div className="p-2 bg-background0 ascii-border flex-shrink-0">
|
||||
<Server className="h-4 w-4" />
|
||||
</div>
|
||||
{(!isCollapsed || !isCollapsed) && (
|
||||
<h2 className="text-sm font-semibold text-foreground truncate">
|
||||
<h2 className="text-sm font-semibold truncate terminal-font">
|
||||
{t("sidebar.systemOverview")}
|
||||
</h2>
|
||||
)}
|
||||
@@ -131,22 +131,22 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
{quickStats ? (
|
||||
<>
|
||||
<div className="w-12 h-12 bg-card/50 border border-border/30 rounded-lg flex flex-col items-center justify-center p-1">
|
||||
<Cpu className="h-3 w-3 text-pink-500 mb-1" />
|
||||
<div className="w-12 h-12 bg-background0 ascii-border flex flex-col items-center justify-center p-1">
|
||||
<Cpu className="h-3 w-3 mb-1" />
|
||||
<span className="text-xs font-bold text-foreground">
|
||||
{quickStats.cpu}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-12 h-12 bg-card/50 border border-border/30 rounded-lg flex flex-col items-center justify-center p-1">
|
||||
<HardDrive className="h-3 w-3 text-cyan-500 mb-1" />
|
||||
<div className="w-12 h-12 bg-background0 ascii-border flex flex-col items-center justify-center p-1">
|
||||
<HardDrive className="h-3 w-3 mb-1" />
|
||||
<span className="text-xs font-bold text-foreground">
|
||||
{quickStats.memory}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-12 h-12 bg-card/50 border border-border/30 rounded-lg flex flex-col items-center justify-center p-1">
|
||||
<Wifi className="h-3 w-3 text-teal-500 mb-1" />
|
||||
<div className="w-12 h-12 bg-background0 ascii-border flex flex-col items-center justify-center p-1">
|
||||
<Wifi className="h-3 w-3 mb-1" />
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xs font-bold text-foreground leading-none">
|
||||
{quickStats.network}
|
||||
@@ -163,7 +163,7 @@ export const Sidebar = forwardRef<HTMLDivElement, SidebarProps>(
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="w-8 h-8 bg-card/50 border border-border/30 rounded-lg flex items-center justify-center"
|
||||
className="w-8 h-8 bg-background2 ascii-border flex items-center justify-center"
|
||||
>
|
||||
<Server className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
@@ -24,33 +24,31 @@ export const TabbedInterface = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-background/80 backdrop-blur-md border border-border/50 rounded-lg p-1 glass-card">
|
||||
<div className="flex">
|
||||
<div className="tui-card p-1 terminal-font">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setActiveTab("cronjobs")}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium transition-all duration-200 rounded-md flex-1 justify-center ${
|
||||
activeTab === "cronjobs"
|
||||
? "bg-primary/10 text-primary shadow-sm border border-primary/20"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
}`}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium transition-all duration-200 flex-1 justify-center terminal-font ${activeTab === "cronjobs"
|
||||
? "bg-background0 ascii-border"
|
||||
: "hover:ascii-border"
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
{t("cronjobs.cronJobs")}
|
||||
<span className="ml-1 text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full font-medium">
|
||||
<span className="ml-1 text-xs bg-background2 px-2 py-0.5 ascii-border font-medium">
|
||||
{cronJobs.length}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("scripts")}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium transition-all duration-200 rounded-md flex-1 justify-center ${
|
||||
activeTab === "scripts"
|
||||
? "bg-primary/10 text-primary shadow-sm border border-primary/20"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
||||
}`}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium transition-all duration-200 flex-1 justify-center terminal-font ${activeTab === "scripts"
|
||||
? "bg-background0 ascii-border"
|
||||
: "hover:ascii-border"
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
{t("scripts.scripts")}
|
||||
<span className="ml-1 text-xs bg-primary/20 text-primary px-2 py-0.5 rounded-full font-medium">
|
||||
<span className="ml-1 text-xs bg-background2 px-2 py-0.5 ascii-border font-medium">
|
||||
{scripts.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -112,8 +112,8 @@ export const LoginForm = ({
|
||||
{hasPassword && hasOIDC
|
||||
? t("login.signInWithPasswordOrSSO")
|
||||
: hasOIDC
|
||||
? t("login.signInWithSSO")
|
||||
: t("login.enterPasswordToContinue")}
|
||||
? t("login.signInWithSSO")
|
||||
: t("login.enterPasswordToContinue")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -175,7 +175,7 @@ export const LoginForm = ({
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
<span className="bg-background0 px-2 text-muted-foreground">
|
||||
{t("login.orContinueWith")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -203,7 +203,7 @@ export const LoginForm = ({
|
||||
</div>
|
||||
|
||||
{version && (
|
||||
<div className="mt-6 pt-4 border-t border-border/50">
|
||||
<div className="mt-6 pt-4 border-t border-border">
|
||||
<div className="text-center text-xs text-muted-foreground">
|
||||
Cr*nMaster {t("common.version", { version })}
|
||||
</div>
|
||||
|
||||
@@ -124,9 +124,9 @@ export const CreateTaskModal = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCustomCommand}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${!form.selectedScriptId
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-border bg-muted/30 text-muted-foreground hover:border-border/60"
|
||||
className={`p-4 rounded-lg transition-all ${!form.selectedScriptId
|
||||
? "border-border border-2"
|
||||
: "border-border border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -145,9 +145,9 @@ export const CreateTaskModal = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSelectScriptModalOpen(true)}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${form.selectedScriptId
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-border bg-muted/30 text-muted-foreground hover:border-border/60"
|
||||
className={`p-4 rounded-lg transition-all ${form.selectedScriptId
|
||||
? "border-border border-2"
|
||||
: "border-border border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -178,7 +178,7 @@ export const CreateTaskModal = ({
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{selectedScript.description}
|
||||
</p>
|
||||
<div className="bg-muted/30 p-2 rounded border border-border/30">
|
||||
<div className="bg-muted/30 p-2 rounded border border-border">
|
||||
<code className="text-xs font-mono text-foreground break-all">
|
||||
{form.command}
|
||||
</code>
|
||||
@@ -222,7 +222,7 @@ export const CreateTaskModal = ({
|
||||
? "/app/scripts/script_name.sh"
|
||||
: "/usr/bin/command"
|
||||
}
|
||||
className="w-full h-24 p-2 border border-border rounded bg-background text-foreground font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
className="w-full h-24 p-2 border border-border rounded bg-background0 text-foreground font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary/20"
|
||||
required
|
||||
readOnly={!!form.selectedScriptId}
|
||||
/>
|
||||
@@ -249,11 +249,11 @@ export const CreateTaskModal = ({
|
||||
value={form.comment}
|
||||
onChange={(e) => onFormChange({ comment: e.target.value })}
|
||||
placeholder={t("cronjobs.whatDoesThisTaskDo")}
|
||||
className="bg-muted/30 border-border/50 focus:border-primary/50"
|
||||
className="bg-muted/30 border-border focus:border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-border/30 bg-muted/10 rounded-lg p-4">
|
||||
<div className="border border-border bg-muted/10 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -279,7 +279,7 @@ export const CreateTaskModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DeleteScriptModal = ({
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Delete Script" size="sm">
|
||||
<div className="space-y-3">
|
||||
<div className="bg-muted/30 rounded p-2 border border-border/50">
|
||||
<div className="bg-muted/30 rounded p-2 border border-border">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="h-3 w-3 text-muted-foreground" />
|
||||
@@ -45,7 +45,7 @@ export const DeleteScriptModal = ({
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<FileText className="h-3 w-3 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<code className="text-xs font-mono bg-muted/30 px-1 py-0.5 rounded border border-border/30">
|
||||
<code className="text-xs font-mono bg-muted/30 px-1 py-0.5 rounded border border-border">
|
||||
{script.filename}
|
||||
</code>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@ export const DeleteScriptModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-border/50">
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DeleteTaskModal = ({
|
||||
size="sm"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-muted/30 rounded p-2 border border-border/50">
|
||||
<div className="bg-muted/30 rounded p-2 border border-border">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
@@ -45,7 +45,7 @@ export const DeleteTaskModal = ({
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<Terminal className="h-3 w-3 text-muted-foreground mt-0.5 flex-shrink-0" />
|
||||
<pre className="max-w-full overflow-x-auto text-xs font-medium text-foreground break-words bg-muted/30 px-1 py-0.5 rounded border border-border/30 flex-1 hide-scrollbar">
|
||||
<pre className="max-w-full overflow-x-auto text-xs font-medium text-foreground break-words bg-muted/30 px-1 py-0.5 rounded border border-border flex-1 hide-scrollbar">
|
||||
{job.command}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@ export const DeleteTaskModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-border/50">
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-border">
|
||||
<Button variant="outline" onClick={onClose} className="btn-outline">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -59,7 +59,7 @@ export const EditTaskModal = ({
|
||||
value={form.command}
|
||||
onChange={(e) => onFormChange({ command: e.target.value })}
|
||||
placeholder="/usr/bin/command"
|
||||
className="font-mono bg-muted/30 border-border/50 focus:border-primary/50"
|
||||
className="font-mono bg-muted/30 border-border focus:border-primary/50"
|
||||
required
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
@@ -80,11 +80,11 @@ export const EditTaskModal = ({
|
||||
value={form.comment}
|
||||
onChange={(e) => onFormChange({ comment: e.target.value })}
|
||||
placeholder={t("cronjobs.whatDoesThisTaskDo")}
|
||||
className="bg-muted/30 border-border/50 focus:border-primary/50"
|
||||
className="bg-muted/30 border-border focus:border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border border-border/30 bg-muted/10 rounded-lg p-4">
|
||||
<div className="border border-border bg-muted/10 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -108,7 +108,7 @@ export const EditTaskModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -69,7 +69,7 @@ Timestamp: ${error.timestamp}
|
||||
<h4 className="text-sm font-medium text-foreground mb-2">
|
||||
Details
|
||||
</h4>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30">
|
||||
<div className="bg-muted/30 p-3 rounded border border-border">
|
||||
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap break-words">
|
||||
{error.details}
|
||||
</pre>
|
||||
@@ -82,7 +82,7 @@ Timestamp: ${error.timestamp}
|
||||
<h4 className="text-sm font-medium text-foreground mb-2">
|
||||
Command
|
||||
</h4>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30">
|
||||
<div className="bg-muted/30 p-3 rounded border border-border">
|
||||
<code className="text-sm font-mono text-foreground break-all">
|
||||
{error.command}
|
||||
</code>
|
||||
@@ -93,7 +93,7 @@ Timestamp: ${error.timestamp}
|
||||
{error.output && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-foreground mb-2">Output</h4>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30 max-h-32 overflow-y-auto">
|
||||
<div className="bg-muted/30 p-3 rounded border border-border max-h-32 overflow-y-auto">
|
||||
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap">
|
||||
{error.output}
|
||||
</pre>
|
||||
@@ -118,7 +118,7 @@ Timestamp: ${error.timestamp}
|
||||
Timestamp: {error.timestamp}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopyDetails}
|
||||
|
||||
@@ -94,17 +94,16 @@ export const FiltersModal = ({
|
||||
</Button>
|
||||
|
||||
{isScheduleDropdownOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background border border-border rounded-md shadow-lg z-50 min-w-[140px]">
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 min-w-[140px]">
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocalScheduleMode("cron");
|
||||
setIsScheduleDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${
|
||||
localScheduleMode === "cron"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}`}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${localScheduleMode === "cron"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
{t("cronjobs.cronSyntax")}
|
||||
@@ -114,11 +113,10 @@ export const FiltersModal = ({
|
||||
setLocalScheduleMode("human");
|
||||
setIsScheduleDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${
|
||||
localScheduleMode === "human"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}`}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${localScheduleMode === "human"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
{t("cronjobs.humanReadable")}
|
||||
@@ -128,11 +126,10 @@ export const FiltersModal = ({
|
||||
setLocalScheduleMode("both");
|
||||
setIsScheduleDropdownOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${
|
||||
localScheduleMode === "both"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}`}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors flex items-center gap-2 ${localScheduleMode === "both"
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
@@ -144,7 +141,7 @@ export const FiltersModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-border">
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
|
||||
@@ -228,19 +228,19 @@ export const LiveLogModal = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<span>{t("cronjobs.liveJobExecution")}{jobComment && `: ${jobComment}`}</span>
|
||||
{status === "running" && (
|
||||
<span className="flex items-center gap-1 text-sm text-blue-500">
|
||||
<span className="flex items-center gap-1 text-sm text-status-info">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{t("cronjobs.running")}
|
||||
</span>
|
||||
)}
|
||||
{status === "completed" && (
|
||||
<span className="flex items-center gap-1 text-sm text-green-500">
|
||||
<span className="flex items-center gap-1 text-sm text-status-success">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
{t("cronjobs.completed", { exitCode: exitCode ?? 0 })}
|
||||
</span>
|
||||
)}
|
||||
{status === "failed" && (
|
||||
<span className="flex items-center gap-1 text-sm text-red-500">
|
||||
<span className="flex items-center gap-1 text-sm text-status-error">
|
||||
<XCircle className="w-4 h-4" />
|
||||
{t("cronjobs.jobFailed", { exitCode: exitCode ?? 1 })}
|
||||
</span>
|
||||
@@ -268,7 +268,7 @@ export const LiveLogModal = ({
|
||||
id="maxLines"
|
||||
value={maxLines}
|
||||
onChange={(e) => setMaxLines(parseInt(e.target.value, 10))}
|
||||
className="bg-background border border-border rounded px-2 py-1 text-sm"
|
||||
className="bg-background0 border border-border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="100">{t("cronjobs.nLines", { count: "100" })}</option>
|
||||
<option value="500">{t("cronjobs.nLines", { count: "500" })}</option>
|
||||
@@ -316,7 +316,7 @@ export const LiveLogModal = ({
|
||||
)}
|
||||
</div>
|
||||
{truncated && !showFullLog && (
|
||||
<div className="text-sm text-orange-500 flex items-center gap-1">
|
||||
<div className="text-sm text-status-warning flex items-center gap-1 terminal-font">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
{t("cronjobs.showingLastOf", {
|
||||
lineCount: lineCount.toLocaleString(),
|
||||
@@ -327,8 +327,8 @@ export const LiveLogModal = ({
|
||||
</div>
|
||||
|
||||
{showSizeWarning && (
|
||||
<div className="bg-orange-500/10 border border-orange-500/30 rounded-lg p-3 flex items-start gap-3">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="bg-background2 ascii-border p-3 flex items-start gap-3 terminal-font">
|
||||
<AlertTriangle className="h-4 w-4 text-status-warning mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-foreground">
|
||||
<span className="font-medium">{t("cronjobs.largeLogFileDetected")}</span> ({formatFileSize(fileSize)})
|
||||
@@ -340,7 +340,7 @@ export const LiveLogModal = ({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={toggleTailMode}
|
||||
className="text-orange-500 hover:text-orange-400 hover:bg-orange-500/10 h-auto py-1 px-2 text-xs"
|
||||
className="text-status-warning hover:text-status-warning hover:bg-background2 h-auto py-1 px-2 text-xs"
|
||||
title={tailMode ? t("cronjobs.showAllLines") : t("cronjobs.enableTailMode")}
|
||||
>
|
||||
{tailMode ? <Maximize2 className="h-3 w-3" /> : <Minimize2 className="h-3 w-3" />}
|
||||
@@ -348,8 +348,8 @@ export const LiveLogModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-black/90 dark:bg-black/60 rounded-lg p-4 max-h-[60vh] overflow-auto">
|
||||
<pre className="text-xs font-mono text-green-400 whitespace-pre-wrap break-words">
|
||||
<div className="bg-black/90 dark:bg-black/60 p-4 max-h-[60vh] overflow-auto terminal-font ascii-border">
|
||||
<pre className="text-xs font-mono text-status-success whitespace-pre-wrap break-words">
|
||||
{logContent || t("cronjobs.waitingForJobToStart")}
|
||||
<div ref={logEndRef} />
|
||||
</pre>
|
||||
|
||||
@@ -208,11 +208,11 @@ export const LogsModal = ({
|
||||
logs.map((log) => (
|
||||
<div
|
||||
key={log.filename}
|
||||
className={`p-3 rounded border cursor-pointer transition-colors ${selectedLog === log.filename
|
||||
? "border-primary bg-primary/10"
|
||||
className={`p-3 ascii-border cursor-pointer transition-colors terminal-font ${selectedLog === log.filename
|
||||
? "border-primary bg-background2"
|
||||
: log.hasError
|
||||
? "border-red-500/50 hover:border-red-500"
|
||||
: "border-border hover:border-primary/50"
|
||||
? "border-red-600 hover:border-red-600"
|
||||
: "ascii-border hover:border-primary"
|
||||
}`}
|
||||
onClick={() => handleViewLog(log.filename)}
|
||||
>
|
||||
@@ -220,9 +220,9 @@ export const LogsModal = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{log.hasError ? (
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0 text-red-500" />
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0 text-status-error" />
|
||||
) : log.exitCode === 0 ? (
|
||||
<CheckCircle className="w-4 h-4 flex-shrink-0 text-green-500" />
|
||||
<CheckCircle className="w-4 h-4 flex-shrink-0 text-status-success" />
|
||||
) : (
|
||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||
)}
|
||||
@@ -236,9 +236,9 @@ export const LogsModal = ({
|
||||
</p>
|
||||
{log.exitCode !== undefined && (
|
||||
<span
|
||||
className={`text-xs px-1.5 py-0.5 rounded ${log.hasError
|
||||
? "bg-red-500/10 text-red-600 dark:text-red-400"
|
||||
: "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
className={`text-xs px-1.5 py-0.5 ${log.hasError
|
||||
? "bg-background2 text-status-error"
|
||||
: "bg-background2 text-status-success"
|
||||
}`}
|
||||
>
|
||||
Exit: {log.exitCode}
|
||||
@@ -271,7 +271,7 @@ export const LogsModal = ({
|
||||
{t("common.loading")}...
|
||||
</div>
|
||||
) : selectedLog ? (
|
||||
<pre className="h-full overflow-auto bg-muted/50 p-4 rounded border border-border text-xs font-mono whitespace-pre-wrap">
|
||||
<pre className="h-full overflow-auto bg-background2 p-4 ascii-border text-xs font-mono whitespace-pre-wrap terminal-font">
|
||||
{logContent}
|
||||
</pre>
|
||||
) : (
|
||||
|
||||
@@ -124,7 +124,7 @@ export const RestoreBackupModal = ({
|
||||
{backups.map((backup) => (
|
||||
<div
|
||||
key={backup.filename}
|
||||
className="glass-card p-3 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors"
|
||||
className="glass-card p-3 border border-border rounded-lg hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
@@ -136,7 +136,7 @@ export const RestoreBackupModal = ({
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{commandCopied === backup.filename && (
|
||||
<Check className="h-3 w-3 text-green-600 flex-shrink-0" />
|
||||
<Check className="h-3 w-3 text-status-success flex-shrink-0" />
|
||||
)}
|
||||
<pre
|
||||
onClick={(e) => {
|
||||
@@ -145,7 +145,7 @@ export const RestoreBackupModal = ({
|
||||
setCommandCopied(backup.filename);
|
||||
setTimeout(() => setCommandCopied(null), 3000);
|
||||
}}
|
||||
className="flex-1 cursor-pointer overflow-hidden text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border/30 truncate"
|
||||
className="flex-1 cursor-pointer overflow-hidden text-sm font-medium text-foreground bg-muted/30 px-2 py-1 rounded border border-border truncate"
|
||||
title={unwrapCommand(backup.job.command)}
|
||||
>
|
||||
{unwrapCommand(backup.job.command)}
|
||||
@@ -204,7 +204,7 @@ export const RestoreBackupModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between gap-2 pt-4 border-t border-border/50">
|
||||
<div className="flex justify-between gap-2 pt-4 border-t border-border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("cronjobs.availableBackups")}: {backups.length}
|
||||
</p>
|
||||
|
||||
@@ -80,11 +80,11 @@ export const ScriptModal = ({
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={title} size="2xl">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-6 terminal-font">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
Script Name <span className="text-red-500">*</span>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Script Name <span className="text-status-error">*</span>
|
||||
</label>
|
||||
<Input
|
||||
value={form.name}
|
||||
@@ -93,15 +93,15 @@ export const ScriptModal = ({
|
||||
required
|
||||
className={
|
||||
!form.name.trim()
|
||||
? "border-red-300 focus:border-red-500 focus:ring-red-500"
|
||||
? "border-status-error focus:border-status-error"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Description{" "}
|
||||
<span className="text-muted-foreground text-xs">(optional)</span>
|
||||
<span className="text-xs opacity-60">(optional)</span>
|
||||
</label>
|
||||
<Input
|
||||
value={form.description}
|
||||
@@ -112,24 +112,24 @@ export const ScriptModal = ({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 h-[500px]">
|
||||
<div className="lg:col-span-1 bg-muted/20 rounded-lg p-4 flex flex-col h-full overflow-hidden">
|
||||
<div className="lg:col-span-1 bg-background0 ascii-border p-4 flex flex-col h-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 mb-4 flex-shrink-0">
|
||||
<Code className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-medium text-foreground">Snippets</h3>
|
||||
<Code className="h-4 w-4" />
|
||||
<h3 className="text-sm font-medium">Snippets</h3>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto min-h-0 !pr-0">
|
||||
<div className="flex-1 overflow-y-auto min-h-0 !pr-0 tui-scrollbar">
|
||||
<BashSnippetHelper onInsertSnippet={handleInsertSnippet} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 flex flex-col h-full overflow-hidden">
|
||||
<div className="flex items-center gap-2 mb-4 flex-shrink-0">
|
||||
<FileText className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Script Content <span className="text-red-500">*</span>
|
||||
<FileText className="h-4 w-4" />
|
||||
<h3 className="text-sm font-medium">
|
||||
Script Content <span className="text-status-error">*</span>
|
||||
</h3>
|
||||
{isDraft && (
|
||||
<span className="ml-auto px-2 py-0.5 text-xs font-medium bg-blue-500/10 text-blue-500 border border-blue-500/30 rounded-full">
|
||||
<span className="ml-auto px-2 py-0.5 text-xs font-medium bg-background0 text-status-info ascii-border">
|
||||
{t("scripts.draft")}
|
||||
</span>
|
||||
)}
|
||||
@@ -145,14 +145,14 @@ export const ScriptModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center gap-3 pt-4 border-t border-border/30">
|
||||
<div className="flex justify-between items-center gap-3 pt-4 ascii-border border-t">
|
||||
<div>
|
||||
{isDraft && onClearDraft && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={onClearDraft}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
className="opacity-60 hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{t("scripts.clearDraft")}
|
||||
|
||||
@@ -124,7 +124,7 @@ export const SelectScriptModal = ({
|
||||
{script.name}
|
||||
</h4>
|
||||
{selectedScriptId === script.id && (
|
||||
<Check className="h-4 w-4 text-green-500 flex-shrink-0" />
|
||||
<Check className="h-4 w-4 text-status-success flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
@@ -167,7 +167,7 @@ export const SelectScriptModal = ({
|
||||
{t("scripts.commandPreview")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30">
|
||||
<div className="bg-muted/30 p-3 rounded border border-border">
|
||||
<code className="text-sm font-mono text-foreground break-all">
|
||||
{hostScriptPath}
|
||||
</code>
|
||||
@@ -178,7 +178,7 @@ export const SelectScriptModal = ({
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t("scripts.scriptContent")}
|
||||
</span>
|
||||
<div className="bg-muted/30 p-3 rounded border border-border/30 mt-2 max-h-32 overflow-auto">
|
||||
<div className="bg-muted/30 p-3 rounded border border-border mt-2 max-h-32 overflow-auto">
|
||||
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
|
||||
{previewContent}
|
||||
</pre>
|
||||
@@ -195,7 +195,7 @@ export const SelectScriptModal = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border/50">
|
||||
<div className="flex justify-end gap-2 pt-3 border-t border-border">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
@@ -42,14 +42,14 @@ export const PWAInstallPrompt = (): JSX.Element | null => {
|
||||
if (choice.outcome === "accepted") {
|
||||
setDeferred(null);
|
||||
}
|
||||
} catch (_err) {}
|
||||
} catch (_err) { }
|
||||
}, [deferred]);
|
||||
|
||||
if (isInstalled || !deferred) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="px-3 py-1 rounded-md border border-border/50 bg-background/80 hover:bg-background/60"
|
||||
className="px-3 py-1 rounded-md border border-border bg-background/80 hover:bg-background/60"
|
||||
onClick={onInstall}
|
||||
>
|
||||
Install App
|
||||
|
||||
@@ -5,7 +5,8 @@ import { EditorView, keymap } from "@codemirror/view";
|
||||
import { EditorState, Transaction } from "@codemirror/state";
|
||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||
import { StreamLanguage } from "@codemirror/language";
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { catppuccinMocha, catppuccinLatte } from './catppuccin-theme';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||
import { Terminal, Copy, Check } from "lucide-react";
|
||||
|
||||
@@ -27,6 +28,7 @@ export const BashEditor = ({
|
||||
const [copied, setCopied] = useState(false);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const editorViewRef = useRef<EditorView | null>(null);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const insertFourSpaces = ({
|
||||
state,
|
||||
@@ -99,13 +101,55 @@ export const BashEditor = ({
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
const isDark = theme === 'catppuccin-mocha';
|
||||
const bashLanguage = StreamLanguage.define(shell);
|
||||
|
||||
// Get CSS variables from the document
|
||||
const getThemeColors = () => {
|
||||
const root = document.documentElement;
|
||||
const style = getComputedStyle(root);
|
||||
|
||||
return {
|
||||
background: style.getPropertyValue('--base').trim() || (isDark ? '#1e1e2e' : '#eff1f5'),
|
||||
foreground: style.getPropertyValue('--text').trim() || (isDark ? '#cdd6f4' : '#4c4f69'),
|
||||
border: style.getPropertyValue('--box-border-color').trim() || (isDark ? '#313244' : '#9ca0b0'),
|
||||
surface: style.getPropertyValue('--surface0').trim() || (isDark ? '#313244' : '#ccd0da'),
|
||||
};
|
||||
};
|
||||
|
||||
const colors = getThemeColors();
|
||||
|
||||
const customTheme = EditorView.theme({
|
||||
"&": {
|
||||
backgroundColor: colors.background,
|
||||
color: colors.foreground,
|
||||
border: `1px solid ${colors.border}`,
|
||||
borderRadius: '0',
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: colors.foreground,
|
||||
padding: '12px'
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: colors.surface,
|
||||
color: colors.foreground,
|
||||
borderRight: `1px solid ${colors.border}`,
|
||||
opacity: '0.6',
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: colors.surface,
|
||||
opacity: '1',
|
||||
},
|
||||
".cm-scroller": {
|
||||
fontFamily: 'JetBrains Mono, Fira Code, monospace',
|
||||
},
|
||||
}, { dark: isDark });
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: value || placeholder,
|
||||
extensions: [
|
||||
bashLanguage,
|
||||
oneDark,
|
||||
customTheme,
|
||||
keymap.of([
|
||||
{ key: "Tab", run: insertFourSpaces },
|
||||
{ key: "Shift-Tab", run: removeFourSpaces },
|
||||
@@ -119,7 +163,7 @@ export const BashEditor = ({
|
||||
"&": {
|
||||
fontSize: "14px",
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
||||
'JetBrains Mono, Fira Code, ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
@@ -132,7 +176,7 @@ export const BashEditor = ({
|
||||
},
|
||||
".cm-scroller": {
|
||||
fontFamily:
|
||||
'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
||||
'JetBrains Mono, Fira Code, ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
|
||||
height: "100%",
|
||||
maxHeight: "100%",
|
||||
},
|
||||
@@ -150,7 +194,7 @@ export const BashEditor = ({
|
||||
return () => {
|
||||
view.destroy();
|
||||
};
|
||||
}, []);
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorViewRef.current) {
|
||||
@@ -181,7 +225,7 @@ export const BashEditor = ({
|
||||
{label && (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-cyan-500" />
|
||||
<Terminal className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
</div>
|
||||
<Button
|
||||
@@ -200,8 +244,8 @@ export const BashEditor = ({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="border border-border overflow-hidden h-full">
|
||||
<div ref={editorRef} className="h-full rounded-lg" />
|
||||
<div className="overflow-hidden h-full">
|
||||
<div ref={editorRef} className="h-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -171,7 +171,7 @@ export const BashSnippetHelper = ({
|
||||
return (
|
||||
<div
|
||||
key={snippet.id}
|
||||
className="bg-muted/30 rounded-lg border border-border/50 p-3 hover:bg-accent/30 transition-colors"
|
||||
className="bg-muted/30 rounded-lg border border-border p-3 hover:bg-accent/30 transition-colors"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
|
||||
@@ -87,9 +87,9 @@ export const CronExpressionHelper = ({
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
{explanation?.isValid ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<CheckCircle className="h-4 w-4 text-status-success" />
|
||||
) : value ? (
|
||||
<AlertCircle className="h-4 w-4 text-red-500" />
|
||||
<AlertCircle className="h-4 w-4 text-status-error" />
|
||||
) : (
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
@@ -97,7 +97,7 @@ export const CronExpressionHelper = ({
|
||||
</div>
|
||||
|
||||
{explanation && (
|
||||
<div className="bg-muted/30 rounded p-2 border border-border/30">
|
||||
<div className="bg-background2 p-2 ascii-border terminal-font">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="h-3 w-3 text-primary mt-0.5 flex-shrink-0" />
|
||||
@@ -108,7 +108,7 @@ export const CronExpressionHelper = ({
|
||||
: "Invalid Expression"}
|
||||
</p>
|
||||
{explanation.error && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400 mt-0.5">
|
||||
<p className="text-xs text-status-error mt-0.5">
|
||||
{explanation.error}
|
||||
</p>
|
||||
)}
|
||||
@@ -117,7 +117,7 @@ export const CronExpressionHelper = ({
|
||||
|
||||
{explanation.isValid && explanation.nextRuns.length > 0 && (
|
||||
<div className="flex items-start gap-2">
|
||||
<Calendar className="h-3 w-3 text-blue-500 mt-0.5 flex-shrink-0" />
|
||||
<Calendar className="h-3 w-3 text-status-info mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-muted-foreground mb-1">
|
||||
Next executions:
|
||||
@@ -137,7 +137,7 @@ export const CronExpressionHelper = ({
|
||||
)}
|
||||
|
||||
{showPatterns && (
|
||||
<div className="bg-muted/30 rounded-lg border border-border/50">
|
||||
<div className="bg-background0 ascii-border terminal-font">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@@ -145,7 +145,7 @@ export const CronExpressionHelper = ({
|
||||
e.stopPropagation();
|
||||
setShowPatternsPanel(!showPatternsPanel);
|
||||
}}
|
||||
className="w-full text-left p-3 hover:bg-accent/30 transition-colors rounded-t-lg"
|
||||
className="w-full text-left p-3 hover:bg-background0 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Quick Patterns</span>
|
||||
@@ -160,7 +160,7 @@ export const CronExpressionHelper = ({
|
||||
</button>
|
||||
|
||||
{showPatternsPanel && (
|
||||
<div className="p-3 border-t border-border/50">
|
||||
<div className="p-3 border-t border-border">
|
||||
<div className="relative mb-3">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
|
||||
@@ -207,7 +207,7 @@ export const ScriptsManager = ({
|
||||
<CardHeader>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<div className="p-2 bg-background2 ascii-border">
|
||||
<FileText className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -230,8 +230,8 @@ export const ScriptsManager = ({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{scripts.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="mx-auto w-20 h-20 bg-gradient-to-br from-primary/20 to-blue-500/20 rounded-full flex items-center justify-center mb-6">
|
||||
<div className="text-center py-16 terminal-font">
|
||||
<div className="mx-auto w-20 h-20 bg-background2 ascii-border flex items-center justify-center mb-6">
|
||||
<FileText className="h-10 w-10 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3 brand-gradient">
|
||||
@@ -254,7 +254,7 @@ export const ScriptsManager = ({
|
||||
{scripts.map((script) => (
|
||||
<div
|
||||
key={script.id}
|
||||
className="glass-card p-4 border border-border/50 rounded-lg hover:bg-accent/30 transition-colors"
|
||||
className="glass-card p-4 ascii-border hover:bg-accent/30 transition-colors terminal-font"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -286,7 +286,7 @@ export const ScriptsManager = ({
|
||||
aria-label="Copy script content to clipboard"
|
||||
>
|
||||
{copiedId === script.id ? (
|
||||
<CheckCircle className="h-3 w-3 text-green-500" />
|
||||
<CheckCircle className="h-3 w-3 text-status-success" />
|
||||
) : (
|
||||
<CopyIcon className="h-3 w-3" />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
|
||||
export const catppuccinMocha: Extension = EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: '#1e1e2e',
|
||||
color: '#cdd6f4',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
border: '1px solid #45475a',
|
||||
borderRadius: '0',
|
||||
},
|
||||
'.cm-content': { caretColor: '#f5e0dc', padding: '12px' },
|
||||
'.cm-gutters': {
|
||||
backgroundColor: '#181825',
|
||||
color: '#6c7086',
|
||||
borderRight: '1px solid #45475a',
|
||||
},
|
||||
}, { dark: true });
|
||||
|
||||
export const catppuccinLatte: Extension = EditorView.theme({
|
||||
'&': {
|
||||
backgroundColor: '#eff1f5',
|
||||
color: '#4c4f69',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
border: '1px solid #9ca0b0',
|
||||
borderRadius: '0',
|
||||
},
|
||||
'.cm-content': { caretColor: '#dc8a78', padding: '12px' },
|
||||
'.cm-gutters': {
|
||||
backgroundColor: '#e6e9ef',
|
||||
color: '#8c8fa1',
|
||||
borderRight: '1px solid #9ca0b0',
|
||||
},
|
||||
}, { dark: false });
|
||||
@@ -20,14 +20,14 @@ export const PerformanceSummary = forwardRef<HTMLDivElement, PerformanceSummaryP
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-3 bg-gradient-to-r from-purple-500/5 to-pink-500/5 border border-purple-500/20 rounded-lg glass-card",
|
||||
"p-3 bg-background0 ascii-border glass-card terminal-font",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Zap className="h-4 w-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-purple-600 dark:text-purple-400">
|
||||
<Zap className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
Performance Summary
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +173,6 @@ export const SystemInfoCard = ({
|
||||
icon: Clock,
|
||||
label: t("sidebar.uptime"),
|
||||
value: systemInfo.uptime,
|
||||
color: "text-orange-500",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -184,7 +183,6 @@ export const SystemInfoCard = ({
|
||||
value: `${systemInfo.memory.used} / ${systemInfo.memory.total}`,
|
||||
detail: `${systemInfo.memory.free} free`,
|
||||
status: systemInfo.memory.status,
|
||||
color: "text-cyan-500",
|
||||
showProgress: true,
|
||||
progressValue: systemInfo.memory.usage,
|
||||
},
|
||||
@@ -194,7 +192,6 @@ export const SystemInfoCard = ({
|
||||
value: systemInfo.cpu.model,
|
||||
detail: `${systemInfo.cpu.cores} cores`,
|
||||
status: systemInfo.cpu.status,
|
||||
color: "text-pink-500",
|
||||
showProgress: true,
|
||||
progressValue: systemInfo.cpu.usage,
|
||||
},
|
||||
@@ -206,7 +203,6 @@ export const SystemInfoCard = ({
|
||||
? `${systemInfo.gpu.memory} VRAM`
|
||||
: systemInfo.gpu.status,
|
||||
status: systemInfo.gpu.status,
|
||||
color: "text-indigo-500",
|
||||
},
|
||||
...(systemInfo.network
|
||||
? [
|
||||
@@ -216,7 +212,6 @@ export const SystemInfoCard = ({
|
||||
value: `${systemInfo.network.latency}ms`,
|
||||
detail: `${systemInfo.network.latency}ms latency • ${systemInfo.network.speed}`,
|
||||
status: systemInfo.network.status,
|
||||
color: "text-teal-500",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
@@ -264,7 +259,6 @@ export const SystemInfoCard = ({
|
||||
icon={item.icon}
|
||||
label={item.label}
|
||||
value={item.value}
|
||||
color={item.color}
|
||||
variant="basic"
|
||||
/>
|
||||
))}
|
||||
@@ -284,7 +278,6 @@ export const SystemInfoCard = ({
|
||||
value={item.value}
|
||||
detail={item.detail}
|
||||
status={item.status}
|
||||
color={item.color}
|
||||
variant="performance"
|
||||
showProgress={item.showProgress}
|
||||
progressValue={item.progressValue}
|
||||
|
||||
@@ -22,27 +22,27 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
|
||||
switch (lowerStatus) {
|
||||
case "operational":
|
||||
return {
|
||||
bgColor: "bg-emerald-500/10",
|
||||
borderColor: "border-emerald-500/20",
|
||||
dotColor: "bg-emerald-500",
|
||||
bgColor: "bg-background0",
|
||||
borderColor: "ascii-border",
|
||||
dotColor: "bg-status-success",
|
||||
};
|
||||
case "warning":
|
||||
return {
|
||||
bgColor: "bg-yellow-500/10",
|
||||
borderColor: "border-yellow-500/20",
|
||||
dotColor: "bg-yellow-500",
|
||||
bgColor: "bg-background0",
|
||||
borderColor: "ascii-border",
|
||||
dotColor: "bg-status-warning",
|
||||
};
|
||||
case "critical":
|
||||
return {
|
||||
bgColor: "bg-destructive/10",
|
||||
borderColor: "border-destructive/20",
|
||||
dotColor: "bg-destructive",
|
||||
bgColor: "bg-background0",
|
||||
borderColor: "ascii-border",
|
||||
dotColor: "bg-status-error",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bgColor: "bg-muted",
|
||||
borderColor: "border-border",
|
||||
dotColor: "bg-muted-foreground",
|
||||
bgColor: "bg-background0",
|
||||
borderColor: "ascii-border",
|
||||
dotColor: "bg-status-success",
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -53,7 +53,7 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-4 border border-border/50 rounded-lg glass-card",
|
||||
"p-4 glass-card terminal-font",
|
||||
config.bgColor,
|
||||
config.borderColor,
|
||||
className
|
||||
@@ -61,7 +61,7 @@ export const SystemStatus = forwardRef<HTMLDivElement, SystemStatusProps>(
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn("w-3 h-3 rounded-full", config.dotColor)} />
|
||||
<div className={cn("w-3 h-3", config.dotColor)} />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@/app/_components/GlobalComponents/UIElements/Button';
|
||||
|
||||
export const ThemeToggle = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
@@ -13,19 +11,16 @@ export const ThemeToggle = () => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
if (!mounted) return null;
|
||||
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
|
||||
<button
|
||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||
className="px-3 py-2 ascii-border terminal-font text-sm bg-background0"
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
{isDark ? 'LIGHT' : 'DARK'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,15 +79,14 @@ export const UserFilter = ({
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background border border-border rounded-md shadow-lg z-50 max-h-48 overflow-y-auto">
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 max-h-48 overflow-y-auto">
|
||||
<button
|
||||
onClick={() => {
|
||||
onUserChange(null);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${
|
||||
!selectedUser ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${!selectedUser ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
{t("common.allUsers")}
|
||||
</button>
|
||||
@@ -98,9 +97,8 @@ export const UserFilter = ({
|
||||
onUserChange(user);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${
|
||||
selectedUser === user ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${selectedUser === user ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
{user}
|
||||
</button>
|
||||
|
||||
@@ -69,7 +69,7 @@ export const UserSwitcher = ({
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background border border-border rounded-md shadow-lg z-50 max-h-48 overflow-y-auto">
|
||||
<div className="absolute top-full left-0 right-0 mt-1 bg-background0 border border-border rounded-md shadow-lg z-50 max-h-48 overflow-y-auto">
|
||||
{users.map((user) => (
|
||||
<button
|
||||
type="button"
|
||||
@@ -80,9 +80,8 @@ export const UserSwitcher = ({
|
||||
onUserChange(user);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${
|
||||
selectedUser === user ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors ${selectedUser === user ? "bg-accent text-accent-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
{user}
|
||||
</button>
|
||||
|
||||
@@ -31,27 +31,24 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
case "operational":
|
||||
case "stable":
|
||||
return {
|
||||
color: "text-emerald-500",
|
||||
bgColor: "bg-emerald-500/10",
|
||||
borderColor: "border-emerald-500/20",
|
||||
color: "text-status-success",
|
||||
bgColor: "bg-background0",
|
||||
icon: CheckCircle,
|
||||
label: t("system.optimal"),
|
||||
};
|
||||
case "moderate":
|
||||
case "warning":
|
||||
return {
|
||||
color: "text-yellow-500",
|
||||
bgColor: "bg-yellow-500/10",
|
||||
borderColor: "border-yellow-500/20",
|
||||
color: "text-status-warning",
|
||||
bgColor: "bg-background0",
|
||||
icon: AlertTriangle,
|
||||
label: t("system.warning"),
|
||||
};
|
||||
case "high":
|
||||
case "slow":
|
||||
return {
|
||||
color: "text-orange-500",
|
||||
bgColor: "bg-orange-500/10",
|
||||
borderColor: "border-orange-500/20",
|
||||
color: "text-status-warning",
|
||||
bgColor: "bg-background0",
|
||||
icon: AlertTriangle,
|
||||
label: t("system.high"),
|
||||
};
|
||||
@@ -59,17 +56,15 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
case "poor":
|
||||
case "offline":
|
||||
return {
|
||||
color: "text-destructive",
|
||||
bgColor: "bg-destructive/10",
|
||||
borderColor: "border-destructive/20",
|
||||
color: "text-status-error",
|
||||
bgColor: "bg-background0",
|
||||
icon: XCircle,
|
||||
label: t("system.critical"),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: "text-muted-foreground",
|
||||
bgColor: "bg-muted",
|
||||
borderColor: "border-border",
|
||||
color: "",
|
||||
bgColor: "bg-background0",
|
||||
icon: Activity,
|
||||
label: t("system.unknown"),
|
||||
};
|
||||
@@ -83,9 +78,8 @@ export const StatusBadge = forwardRef<HTMLDivElement, StatusBadgeProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-full border px-2 py-1",
|
||||
"inline-flex items-center gap-1.5 ascii-border px-2 py-1 terminal-font",
|
||||
config.bgColor,
|
||||
config.borderColor,
|
||||
{
|
||||
"text-xs": size === "sm",
|
||||
"text-sm": size === "md",
|
||||
|
||||
@@ -1,72 +1,43 @@
|
||||
import { cn } from '@/app/_utils/global-utils';
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
export const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
({ className = '', ...props }, ref) => (
|
||||
<div ref={ref} className={`tui-card ${className}`} {...props} />
|
||||
)
|
||||
);
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
<div ref={ref} className={`p-4 border-b border-foreground1 ${className}`} {...props} />
|
||||
)
|
||||
);
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-2xl font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
<h3 ref={ref} className={`terminal-font font-bold uppercase ${className}`} {...props} />
|
||||
)
|
||||
);
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
<p ref={ref} className={`terminal-font text-sm ${className}`} {...props} />
|
||||
)
|
||||
);
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-4 lg:p-6 pt-0', className)} {...props} />
|
||||
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
<div ref={ref} className={`p-4 ${className}`} {...props} />
|
||||
)
|
||||
);
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
||||
({ className = '', ...props }, ref) => (
|
||||
<div ref={ref} className={`flex items-center p-4 border-t border-foreground1 ${className}`} {...props} />
|
||||
)
|
||||
);
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
|
||||
@@ -27,7 +27,7 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
|
||||
value,
|
||||
detail,
|
||||
status,
|
||||
color = "text-blue-500",
|
||||
color,
|
||||
variant = "basic",
|
||||
showProgress = false,
|
||||
progressValue = 0,
|
||||
@@ -40,14 +40,14 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-3 border border-border/50 rounded-lg hover:bg-accent/50 transition-colors duration-200 glass-card-hover",
|
||||
"flex items-start gap-3 p-3 tui-card-mini transition-colors duration-200 terminal-font",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"p-2 rounded-lg border border-border/50 flex-shrink-0 bg-card/50"
|
||||
"p-2 ascii-border flex-shrink-0 bg-background0"
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-4 w-4", color)} />
|
||||
@@ -55,7 +55,7 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
<p className="text-xs font-medium uppercase tracking-wide terminal-font">
|
||||
{label}
|
||||
</p>
|
||||
{status && variant === "performance" && (
|
||||
@@ -67,12 +67,12 @@ export const MetricCard = forwardRef<HTMLDivElement, MetricCardProps>(
|
||||
<TruncatedText
|
||||
text={value}
|
||||
maxLength={40}
|
||||
className="text-sm font-medium text-foreground"
|
||||
className="text-sm font-medium terminal-font"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{detail && (
|
||||
<p className="text-xs text-muted-foreground mb-2">{detail}</p>
|
||||
<p className="text-xs mb-2 terminal-font">{detail}</p>
|
||||
)}
|
||||
|
||||
{showProgress && (
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { cn } from '@/app/_utils/global-utils';
|
||||
import { InputHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> { }
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
({ className = '', ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
className={`terminal-font ascii-border px-3 py-2 bg-background0 w-full ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn } from '@/app/_utils/global-utils';
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -7,30 +6,31 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'default', size = 'default', ...props }, ref) => {
|
||||
({ className = '', variant = 'default', size = 'default', children, ...props }, ref) => {
|
||||
const baseClasses = 'terminal-font ascii-border px-4 py-2 cursor-pointer inline-flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
const variantClasses = {
|
||||
default: 'bg-background1 hover:bg-background2',
|
||||
destructive: 'bg-status-error text-white hover:bg-status-error',
|
||||
outline: 'bg-background0 hover:bg-background1',
|
||||
secondary: 'bg-background2 hover:bg-background1',
|
||||
ghost: 'border-0 bg-transparent hover:bg-background1',
|
||||
link: 'border-0 underline bg-transparent',
|
||||
};
|
||||
const sizeClasses = {
|
||||
default: '',
|
||||
sm: 'px-2 py-1 text-sm',
|
||||
lg: 'px-6 py-3',
|
||||
icon: 'p-2',
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground': variant === 'outline',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
|
||||
'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
|
||||
'text-primary underline-offset-4 hover:underline': variant === 'link',
|
||||
},
|
||||
{
|
||||
'h-10 px-4 py-2': size === 'default',
|
||||
'h-9 rounded-md px-3': size === 'sm',
|
||||
'h-11 rounded-md px-8': size === 'lg',
|
||||
'h-10 w-10': size === 'icon',
|
||||
},
|
||||
className
|
||||
)}
|
||||
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -98,7 +98,7 @@ export const DropdownMenu = ({
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`absolute right-0 w-56 rounded-lg border border-border/50 bg-background shadow-lg z-[9999] overflow-hidden ${
|
||||
className={`absolute right-0 w-56 ascii-border bg-background0 shadow-lg z-[9999] overflow-hidden terminal-font ${
|
||||
positionAbove ? "bottom-full mb-2" : "top-full mt-2"
|
||||
}`}
|
||||
>
|
||||
@@ -112,8 +112,8 @@ export const DropdownMenu = ({
|
||||
item.disabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: item.variant === "destructive"
|
||||
? "text-destructive hover:bg-destructive/10"
|
||||
: "text-foreground hover:bg-accent"
|
||||
? "text-status-error hover:bg-background1"
|
||||
: "hover:bg-background1"
|
||||
}`}
|
||||
>
|
||||
{item.icon && (
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/app/_utils/global-utils";
|
||||
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
|
||||
import { Button } from "./Button";
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -26,99 +25,51 @@ export const Modal = ({
|
||||
preventCloseOnClickOutside = false,
|
||||
className = "",
|
||||
}: ModalProps) => {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const dialogRef = useRef<HTMLDialogElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const dialog = dialogRef.current;
|
||||
if (!dialog) return;
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
dialog.showModal();
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
} else {
|
||||
dialog.close();
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
modalRef.current &&
|
||||
!modalRef.current.contains(event.target as Node) &&
|
||||
!preventCloseOnClickOutside
|
||||
) {
|
||||
const target = event.target as Element;
|
||||
const isClickingOnModal = target.closest('[data-modal="true"]');
|
||||
const isClickingOnBackdrop =
|
||||
target.classList.contains("modal-backdrop");
|
||||
|
||||
if (isClickingOnBackdrop) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen, onClose, preventCloseOnClickOutside]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
}, [isOpen]);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "max-w-md",
|
||||
md: "max-w-lg",
|
||||
lg: "max-w-2xl",
|
||||
xl: "max-w-4xl",
|
||||
"2xl": "max-w-6xl",
|
||||
"3xl": "max-w-8xl",
|
||||
sm: "w-[600px]",
|
||||
md: "w-[800px]",
|
||||
lg: "w-[1000px]",
|
||||
xl: "w-[1200px]",
|
||||
"2xl": "w-[1400px]",
|
||||
"3xl": "w-[90vw]",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-end justify-center sm:items-center p-0 sm:p-4"
|
||||
data-modal="true"
|
||||
<dialog
|
||||
ref={dialogRef}
|
||||
className={`ascii-border terminal-font bg-background0 ${sizeClasses[size]} max-w-[95vw] ${className}`}
|
||||
onClick={(e) => {
|
||||
if (e.target === dialogRef.current && !preventCloseOnClickOutside) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm modal-backdrop" />
|
||||
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={cn(
|
||||
"relative w-full bg-card border border-border shadow-lg",
|
||||
"max-h-[85vh]",
|
||||
"sm:rounded-lg sm:max-h-[90vh] sm:w-full",
|
||||
sizeClasses[size],
|
||||
className
|
||||
<div className="ascii-border border-t-0 border-l-0 border-r-0 p-4 flex justify-between items-center bg-background0">
|
||||
<h2 className="terminal-font font-bold uppercase">{title}</h2>
|
||||
{showCloseButton && (
|
||||
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-border sticky top-0 bg-card z-10">
|
||||
<h2 className="text-lg font-semibold text-foreground">{title}</h2>
|
||||
{showCloseButton && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(80vh-100px)]">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 max-h-[70vh] overflow-y-auto tui-scrollbar bg-background0">
|
||||
{children}
|
||||
</div>
|
||||
</dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,35 +25,29 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
|
||||
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
||||
|
||||
const getColorClass = (percentage: number) => {
|
||||
if (percentage >= 90) return "bg-destructive";
|
||||
if (percentage >= 80) return "bg-orange-500";
|
||||
if (percentage >= 70) return "bg-yellow-500";
|
||||
return "bg-emerald-500";
|
||||
if (percentage >= 90) return "bg-red-600";
|
||||
if (percentage >= 80) return "bg-yellow-600";
|
||||
if (percentage >= 70) return "bg-yellow-600";
|
||||
return "bg-green-600";
|
||||
};
|
||||
|
||||
const getGradientClass = (percentage: number) => {
|
||||
if (percentage >= 90)
|
||||
return "bg-gradient-to-r from-destructive to-red-600";
|
||||
if (percentage >= 80)
|
||||
return "bg-gradient-to-r from-orange-500 to-orange-600";
|
||||
if (percentage >= 70)
|
||||
return "bg-gradient-to-r from-yellow-500 to-yellow-600";
|
||||
return "bg-gradient-to-r from-emerald-500 to-emerald-600";
|
||||
return getColorClass(percentage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("w-full", className)} {...props}>
|
||||
<div ref={ref} className={cn("w-full terminal-font", className)} {...props}>
|
||||
{showLabel && (
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs text-muted-foreground">Usage</span>
|
||||
<span className="text-xs font-medium text-foreground">
|
||||
<span className="text-xs">Usage</span>
|
||||
<span className="text-xs font-medium">
|
||||
{Math.round(percentage)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn("w-full bg-muted rounded-full overflow-hidden", {
|
||||
className={cn("w-full bg-background2 ascii-border overflow-hidden", {
|
||||
"h-1.5": size === "sm",
|
||||
"h-2": size === "md",
|
||||
"h-3": size === "lg",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/app/_utils/global-utils";
|
||||
|
||||
interface SwitchProps {
|
||||
checked: boolean;
|
||||
onCheckedChange: (checked: boolean) => void;
|
||||
@@ -9,27 +7,16 @@ interface SwitchProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Switch = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
className = "",
|
||||
disabled = false,
|
||||
}: SwitchProps) => {
|
||||
export const Switch = ({ checked, onCheckedChange, className = "", disabled = false }: SwitchProps) => {
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"relative inline-flex items-center cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<label className={className}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={checked}
|
||||
onChange={(e) => onCheckedChange(e.target.checked)}
|
||||
disabled={disabled}
|
||||
className="terminal-font"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-muted peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-primary/25 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-primary peer-disabled:opacity-50 peer-disabled:cursor-not-allowed"></div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,12 +37,10 @@ const toastIcons = {
|
||||
};
|
||||
|
||||
const toastStyles = {
|
||||
success:
|
||||
"border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-400",
|
||||
error: "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-400",
|
||||
info: "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-400",
|
||||
warning:
|
||||
"border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
|
||||
success: "ascii-border bg-background0 text-status-success",
|
||||
error: "ascii-border bg-background0 text-status-error",
|
||||
info: "ascii-border bg-background0 text-status-info",
|
||||
warning: "ascii-border bg-background0 text-status-warning",
|
||||
};
|
||||
|
||||
export const Toast = ({ toast, onRemove, onErrorClick }: ToastProps) => {
|
||||
@@ -62,7 +60,7 @@ export const Toast = ({ toast, onRemove, onErrorClick }: ToastProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-3 p-4 rounded-lg border backdrop-blur-md transition-all duration-300 ease-in-out",
|
||||
"flex items-start gap-3 p-4 terminal-font transition-all duration-300 ease-in-out",
|
||||
toastStyles[toast.type],
|
||||
isVisible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,21 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import { type ThemeProviderProps } from 'next-themes/dist/types';
|
||||
|
||||
export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="data-webtui-theme"
|
||||
defaultTheme="light"
|
||||
themes={['light', 'dark']}
|
||||
value={{
|
||||
light: 'catppuccin-latte',
|
||||
dark: 'catppuccin-mocha',
|
||||
}}
|
||||
disableTransitionOnChange
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
466
app/globals.css
466
app/globals.css
@@ -1,325 +1,187 @@
|
||||
/* eslint-disable */
|
||||
/* @ts-nocheck */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 280 100% 60%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 280 100% 60%;
|
||||
--radius: 0.75rem;
|
||||
--chart-1: 280 100% 60%;
|
||||
--chart-2: 160 84% 39%;
|
||||
--chart-3: 30 100% 50%;
|
||||
--chart-4: 340 100% 50%;
|
||||
--chart-5: 200 100% 50%;
|
||||
[data-webtui-theme="catppuccin-latte"] {
|
||||
--box-border-color: #9ca0b0;
|
||||
--table-border-color: #9ca0b0;
|
||||
--separator-color: #9ca0b0;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 8%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 12%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 12%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 280 100% 40%;
|
||||
--primary-foreground: 240 10% 8%;
|
||||
--secondary: 240 3.7% 18%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 18%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 18%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 25%;
|
||||
--input: 240 3.7% 18%;
|
||||
--ring: 280 100% 40%;
|
||||
--chart-1: 280 100% 40%;
|
||||
--chart-2: 160 84% 30%;
|
||||
--chart-3: 30 100% 35%;
|
||||
--chart-4: 340 100% 35%;
|
||||
--chart-5: 200 100% 35%;
|
||||
[data-webtui-theme="catppuccin-mocha"] {
|
||||
--box-border-color: #313244;
|
||||
--table-border-color: #313244;
|
||||
--separator-color: #313244;
|
||||
}
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.overflow-y-auto {
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
.overflow-y-auto::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.overflow-y-auto::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--primary) / 0.8);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
font-variation-settings: normal;
|
||||
}
|
||||
|
||||
code, pre, .font-mono {
|
||||
font-family: var(--font-mono);
|
||||
font-feature-settings: "liga" 1, "calt" 1;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button, input, textarea, select {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.terminal-text {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.hero-gradient {
|
||||
background: linear-gradient(135deg,
|
||||
hsl(280 100% 60% / 0.1) 0%,
|
||||
hsl(160 84% 39% / 0.05) 25%,
|
||||
hsl(30 100% 50% / 0.05) 50%,
|
||||
hsl(340 100% 50% / 0.05) 75%,
|
||||
hsl(200 100% 50% / 0.1) 100%);
|
||||
}
|
||||
|
||||
.hero-gradient::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, hsl(280 100% 60% / 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, hsl(160 84% 39% / 0.15) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, hsl(340 100% 50% / 0.1) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dark .hero-gradient::before {
|
||||
background:
|
||||
radial-gradient(circle at 20% 80%, hsl(280 100% 50% / 0.08) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, hsl(160 84% 35% / 0.08) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, hsl(340 100% 45% / 0.06) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
@apply backdrop-blur-md bg-card/80 border border-border/50;
|
||||
}
|
||||
|
||||
.dark .glass-card {
|
||||
@apply backdrop-blur-md bg-card/80 border border-border/70;
|
||||
}
|
||||
|
||||
.glass-card-hover {
|
||||
@apply glass-card;
|
||||
}
|
||||
|
||||
.brand-gradient {
|
||||
@apply bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.dark .brand-gradient {
|
||||
@apply bg-gradient-to-r from-purple-600 via-pink-600 to-orange-600 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.brand-gradient-alt {
|
||||
@apply bg-gradient-to-r from-cyan-500 via-blue-500 to-purple-500 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.dark .brand-gradient-alt {
|
||||
@apply bg-gradient-to-r from-cyan-600 via-blue-600 to-purple-600 bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
.glow-primary {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.glow-primary:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.glow-cyan {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.glow-orange {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.glow-pink {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
@apply bg-red-500/20 text-red-700 dark:text-red-400 border-red-500/30;
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: hsl(var(--muted-foreground) / 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
@apply absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 rounded-lg shadow-lg opacity-0 invisible transition-all duration-200;
|
||||
}
|
||||
|
||||
.tooltip.show {
|
||||
@apply opacity-100 visible;
|
||||
}
|
||||
|
||||
.text-responsive {
|
||||
@apply text-sm sm:text-base lg:text-lg;
|
||||
}
|
||||
|
||||
.text-responsive-lg {
|
||||
@apply text-base sm:text-lg lg:text-xl;
|
||||
}
|
||||
|
||||
.text-responsive-xl {
|
||||
@apply text-lg sm:text-xl lg:text-2xl;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 transition-all;
|
||||
}
|
||||
|
||||
.dark .btn-primary {
|
||||
@apply bg-gradient-to-r from-purple-600 to-pink-600 text-white hover:from-purple-700 hover:to-pink-700 transition-all;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-gradient-to-r from-cyan-500 to-blue-500 text-white hover:from-cyan-600 hover:to-blue-600 transition-all;
|
||||
}
|
||||
|
||||
.dark .btn-secondary {
|
||||
@apply bg-gradient-to-r from-cyan-600 to-blue-600 text-white hover:from-cyan-700 hover:to-blue-700 transition-all;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
@apply border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
@apply hover:bg-accent hover:text-accent-foreground transition-colors;
|
||||
}
|
||||
|
||||
.btn-destructive {
|
||||
@apply bg-gradient-to-r from-red-500 to-pink-500 text-white hover:from-red-600 hover:to-pink-600 transition-all;
|
||||
}
|
||||
|
||||
.dark .btn-destructive {
|
||||
@apply bg-gradient-to-r from-red-600 to-pink-600 text-white hover:from-red-700 hover:to-pink-700 transition-all;
|
||||
}
|
||||
|
||||
.neon-border {
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(white, white) padding-box,
|
||||
linear-gradient(45deg, hsl(280 100% 60%), hsl(160 84% 39%), hsl(30 100% 50%)) border-box;
|
||||
}
|
||||
|
||||
.neon-border-dark {
|
||||
border: 1px solid transparent;
|
||||
background: linear-gradient(hsl(240 10% 3.9%), hsl(240 10% 3.9%)) padding-box,
|
||||
linear-gradient(45deg, hsl(280 100% 70%), hsl(160 84% 45%), hsl(30 100% 60%)) border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
body.sidebar-collapsed main.lg\:ml-80 {
|
||||
margin-left: 4rem !important;
|
||||
|
||||
|
||||
.ascii-border {
|
||||
border: 1px solid var(--box-border-color, var(--foreground2));
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.border-border {
|
||||
border-color: var(--box-border-color, var(--foreground2))!important;
|
||||
}
|
||||
|
||||
.tui-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--box-border-color, var(--foreground2)) var(--background1);
|
||||
}
|
||||
|
||||
.tui-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tui-scrollbar::-webkit-scrollbar-track {
|
||||
background: var(--background1);
|
||||
}
|
||||
|
||||
.tui-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--box-border-color, var(--foreground2));
|
||||
}
|
||||
|
||||
.bg-background0 {
|
||||
background-color: var(--background0) !important;
|
||||
}
|
||||
|
||||
.bg-background1 {
|
||||
background-color: var(--background1) !important;
|
||||
}
|
||||
|
||||
.bg-background2 {
|
||||
background-color: var(--background2) !important;
|
||||
}
|
||||
|
||||
.border-foreground1 {
|
||||
border-color: var(--box-border-color, var(--foreground2)) !important;
|
||||
}
|
||||
|
||||
.text-foreground0 {
|
||||
color: var(--foreground0) !important;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-overflow-fix {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
@layer components {
|
||||
.tui-card {
|
||||
background: var(--background0) !important;
|
||||
border: 1px solid var(--box-border-color, var(--foreground2)) !important;
|
||||
box-shadow: 8px 4px 0 var(--box-border-color, var(--foreground2));
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.dropdown-overflow-fix > * {
|
||||
position: relative;
|
||||
}
|
||||
.tui-card-mini {
|
||||
background: var(--background0) !important;
|
||||
border: 1px solid var(--box-border-color, var(--foreground2)) !important;
|
||||
box-shadow: 2px 4px 0 var(--box-border-color, var(--foreground2));
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.dropdown-overflow-fix .dropdown-container,
|
||||
.dropdown-overflow-fix [class*="dropdown"] {
|
||||
overflow: visible !important;
|
||||
.terminal-log {
|
||||
background: var(--background0) !important;
|
||||
border: 1px solid var(--box-border-color, var(--foreground2)) !important;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
padding: 1rem;
|
||||
color: var(--foreground0);
|
||||
}
|
||||
|
||||
.text-status-info {
|
||||
color: var(--mauve);
|
||||
}
|
||||
|
||||
.text-status-warning {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.text-status-success {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.text-status-error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.bg-status-info {
|
||||
background-color: var(--mauve);
|
||||
}
|
||||
|
||||
.bg-status-warning {
|
||||
background-color: var(--yellow);
|
||||
}
|
||||
|
||||
.bg-status-success {
|
||||
background-color: var(--green);
|
||||
}
|
||||
|
||||
.bg-status-error {
|
||||
background-color: var(--red);
|
||||
}
|
||||
|
||||
dialog {
|
||||
background: var(--background0) !important;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
width: 90vw;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
|
||||
dialog[open] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Sidebar layout adjustment */
|
||||
.no-sidebar main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
body:not(.sidebar-collapsed) main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body:not(.sidebar-collapsed) main {
|
||||
margin-left: 320px;
|
||||
}
|
||||
|
||||
body.sidebar-collapsed main {
|
||||
margin-left: 64px;
|
||||
}
|
||||
|
||||
.no-sidebar main {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { JetBrains_Mono, Inter } from "next/font/google";
|
||||
import { JetBrains_Mono } from "next/font/google";
|
||||
import "@/app/globals.css";
|
||||
import { ThemeProvider } from "@/app/_providers/ThemeProvider";
|
||||
import { ServiceWorkerRegister } from "@/app/_components/FeatureComponents/PWA/ServiceWorkerRegister";
|
||||
@@ -13,12 +13,6 @@ const jetbrainsMono = JetBrains_Mono({
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Cr*nMaster - Cron Management made easy",
|
||||
description:
|
||||
@@ -58,7 +52,7 @@ export default async function RootLayout({
|
||||
messages = await loadTranslationMessages(locale);
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang="en" suppressHydrationWarning data-webtui-theme="catppuccin-latte">
|
||||
<head>
|
||||
<meta name="application-name" content="Cr*nMaster" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
@@ -66,15 +60,23 @@ export default async function RootLayout({
|
||||
<meta name="apple-mobile-web-app-title" content="Cr*nMaster" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<link rel="stylesheet" href="/webtui/base.css" />
|
||||
<link rel="stylesheet" href="/webtui/theme-catppuccin.css" />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
const webtui = theme === 'dark' ? 'catppuccin-mocha' : 'catppuccin-latte';
|
||||
document.documentElement.setAttribute('data-webtui-theme', webtui);
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans`}>
|
||||
<body className={`${jetbrainsMono.variable} terminal-font`}>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
<ServiceWorkerRegister />
|
||||
|
||||
70
app/page.tsx
70
app/page.tsx
@@ -9,6 +9,7 @@ import { PWAInstallPrompt } from "@/app/_components/FeatureComponents/PWA/PWAIns
|
||||
import { WrapperScriptWarning } from "@/app/_components/FeatureComponents/System/WrapperScriptWarning";
|
||||
import { getTranslations } from "@/app/_server/actions/translations";
|
||||
import { SSEProvider } from "@/app/_contexts/SSEContext";
|
||||
import { FcClock, FcDocument } from "react-icons/fc";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const maxDuration = 300;
|
||||
@@ -59,52 +60,49 @@ export default async function Home() {
|
||||
},
|
||||
};
|
||||
|
||||
const bodyClass = process.env.DISABLE_SYSTEM_STATS === "true" ? "no-sidebar" : "";
|
||||
|
||||
return (
|
||||
<SSEProvider liveUpdatesEnabled={liveUpdatesEnabled}>
|
||||
<div className="min-h-screen relative">
|
||||
<div className="hero-gradient absolute inset-0 -z-10"></div>
|
||||
<div className="relative z-10">
|
||||
<header className="border-b border-border/50 bg-background/80 backdrop-blur-md sticky top-0 z-20 shadow-sm lg:h-[90px]">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between lg:justify-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<img src="/logo.png" alt="logo" className="w-14 h-14" />
|
||||
<div className="absolute top-0 right-0 w-3 h-3 bg-emerald-500 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold brand-gradient brand-text">
|
||||
Cr*nMaster
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground font-mono tracking-wide">
|
||||
{t("common.cronManagementMadeEasy")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`min-h-screen bg-background0 ${bodyClass}`}>
|
||||
<header className="ascii-border !border-r-0 sticky top-0 z-20 bg-background0 lg:h-[90px]">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between lg:justify-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<FcClock size={48} />
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl lg:text-3xl font-bold terminal-font uppercase">
|
||||
Cr*nMaster
|
||||
</h1>
|
||||
<p className="text-xs terminal-font flex items-center gap-2">
|
||||
<FcDocument size={16} />
|
||||
{t("common.cronManagementMadeEasy")}
|
||||
</p>
|
||||
</div>
|
||||
{process.env.AUTH_PASSWORD && (
|
||||
<div className="lg:absolute lg:right-10">
|
||||
<LogoutButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{process.env.AUTH_PASSWORD && (
|
||||
<div className="lg:absolute lg:right-10">
|
||||
<LogoutButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{process.env.DISABLE_SYSTEM_STATS !== "true" && (
|
||||
<SystemInfoCard systemInfo={initialSystemInfo} />
|
||||
)}
|
||||
{process.env.DISABLE_SYSTEM_STATS !== "true" && (
|
||||
<SystemInfoCard systemInfo={initialSystemInfo} />
|
||||
)}
|
||||
|
||||
<main className={`${process.env.DISABLE_SYSTEM_STATS === "true" ? "lg:ml-0" : "lg:ml-80"} transition-all duration-300 ml-0 sidebar-collapsed:lg:ml-16`}>
|
||||
<div className="container mx-auto px-4 py-8 lg:px-8">
|
||||
<WrapperScriptWarning />
|
||||
<TabbedInterface cronJobs={cronJobs} scripts={scripts} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<main className="transition-all duration-300">
|
||||
<div className="container mx-auto px-4 py-8 lg:px-8">
|
||||
<WrapperScriptWarning />
|
||||
<TabbedInterface cronJobs={cronJobs} scripts={scripts} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ToastContainer />
|
||||
|
||||
<div className="flex items-center gap-2 fixed bottom-4 left-4 lg:right-4 lg:left-auto z-10 bg-background/80 backdrop-blur-md border border-border/50 rounded-lg p-1">
|
||||
<div className="flex items-center gap-2 fixed bottom-4 left-4 lg:right-4 lg:left-auto z-10 bg-background0 ascii-border p-1">
|
||||
<ThemeToggle />
|
||||
<PWAInstallPrompt />
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@webtui/css": "^0.1.5",
|
||||
"@webtui/theme-catppuccin": "^0.0.3",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^2.0.0",
|
||||
@@ -40,6 +42,7 @@
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"systeminformation": "^5.27.11",
|
||||
"tailwind-merge": "^2.0.0",
|
||||
@@ -50,6 +53,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/minimatch": "^6.0.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.0.4"
|
||||
"eslint-config-next": "14.0.4",
|
||||
"postcss-import": "^16.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
|
||||
1
public/webtui/base.css
Normal file
1
public/webtui/base.css
Normal file
@@ -0,0 +1 @@
|
||||
@layer base{:root{--background0: #fff;--background1: #ddd;--background2: #bbb;--background3: #999;--foreground0: #000;--foreground1: #444;--foreground2: #888;--font-size: 16px;--line-height: 1.3;--font-weight-bold: 700;--font-weight-normal: 400;--font-family: monospace;--box-border-color: var(--foreground0);--table-border-color: var(--box-border-color);--separator-color: var(--box-border-color);--separator-background: transparent}[data-webtui-theme=dark]{--background0: #000;--background1: #222;--background2: #444;--background3: #666;--foreground0: #fff;--foreground1: #ccc;--foreground2: #999}body,html{background-color:var(--background0);color:var(--foreground0);font-family:var(--font-family);font-size:var(--font-size);font-weight:var(--font-weight-normal);line-height:var(--line-height, 1.5);font-variant-ligatures:common-ligatures}*{box-sizing:border-box;margin:0;padding:0;outline:none}}
|
||||
1
public/webtui/theme-catppuccin.css
Normal file
1
public/webtui/theme-catppuccin.css
Normal file
File diff suppressed because one or more lines are too long
24
yarn.lock
24
yarn.lock
@@ -1700,6 +1700,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777"
|
||||
integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==
|
||||
|
||||
"@webtui/css@^0.1.5":
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@webtui/css/-/css-0.1.5.tgz#2a1d2fe50200ee1dfbbe87448acc0298c3a0d5a6"
|
||||
integrity sha512-6+yk/XjW4JHaRUJpglYcvE9pcxay+PmoPFYeedHksCz0JHp3POW0LG2c6q1hSu2y7U4V0yHO7/eQBC3P0qCyIw==
|
||||
|
||||
"@webtui/theme-catppuccin@^0.0.3":
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@webtui/theme-catppuccin/-/theme-catppuccin-0.0.3.tgz#bc451e8334f697acd17aeb31b721c17012edd37a"
|
||||
integrity sha512-mn3qpyIDYzPm9DoDX7Rs/Ma/3YawbAZEu6/Cemj4kTodr4eBFuEOmkkd1RtChsXGH75fZ054MIzZoVwYvA5W2g==
|
||||
|
||||
acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -4160,6 +4170,15 @@ postcss-import@^15.1.0:
|
||||
read-cache "^1.0.0"
|
||||
resolve "^1.1.7"
|
||||
|
||||
postcss-import@^16.1.1:
|
||||
version "16.1.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-16.1.1.tgz#cfbe79e6c9232b0dbbe1c18f35308825cfe8ff2a"
|
||||
integrity sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==
|
||||
dependencies:
|
||||
postcss-value-parser "^4.0.0"
|
||||
read-cache "^1.0.0"
|
||||
resolve "^1.1.7"
|
||||
|
||||
postcss-js@^4.0.1:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.1.0.tgz#003b63c6edde948766e40f3daf7e997ae43a5ce6"
|
||||
@@ -4282,6 +4301,11 @@ react-dom@^18:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-icons@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
|
||||
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
||||
Reference in New Issue
Block a user