From e313029db8a632cda4681810f29d396b425424fc Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 17 Oct 2025 14:57:08 +0200 Subject: [PATCH 01/10] feat: implement light/dark mode theme system - Add semantic color CSS variables (success, warning, info, error) for both themes - Create ThemeProvider with React context and localStorage persistence - Add ThemeToggle component with sun/moon icons for header region - Add theme switcher in General Settings modal - Replace 200+ hardcoded Tailwind colors with CSS variables across 30+ components - Update layout.tsx to remove forced dark mode - Keep terminal colors unchanged as requested - Default to dark mode, with seamless light/dark switching Components updated: - High-priority: InstalledScriptsTab, ScriptInstallationCard, LXCSettingsModal, ScriptsGrid - All remaining component files with hardcoded colors - UI components: button, toggle, badge variants - Modal components: ErrorModal, ConfirmationModal, AuthModal, SetupModal - Form components: ServerForm, FilterBar, CategorySidebar - Display components: ScriptCard, ScriptCardList, DiffViewer, TextViewer Theme switchers: - Header: Small nuanced toggle in top-right - Settings: Detailed Light/Dark selection in General Settings --- src/app/_components/AuthGuard.tsx | 2 +- src/app/_components/AuthModal.tsx | 4 +- src/app/_components/Badge.tsx | 18 ++-- src/app/_components/CategorySidebar.tsx | 6 +- src/app/_components/ConfirmationModal.tsx | 4 +- src/app/_components/DiffViewer.tsx | 8 +- src/app/_components/DownloadedScriptsTab.tsx | 4 +- src/app/_components/ErrorModal.tsx | 16 +-- src/app/_components/ExecutionModeModal.tsx | 4 +- src/app/_components/FilterBar.tsx | 8 +- src/app/_components/GeneralSettingsModal.tsx | 49 +++++++-- src/app/_components/HelpModal.tsx | 18 ++-- src/app/_components/InstalledScriptsTab.tsx | 100 +++++++++--------- src/app/_components/LXCSettingsModal.tsx | 40 +++---- src/app/_components/PublicKeyModal.tsx | 4 +- src/app/_components/ReleaseNotesModal.tsx | 8 +- src/app/_components/ResyncButton.tsx | 4 +- src/app/_components/ScriptCard.tsx | 6 +- src/app/_components/ScriptCardList.tsx | 6 +- src/app/_components/ScriptDetailModal.tsx | 14 +-- .../_components/ScriptInstallationCard.tsx | 40 +++---- src/app/_components/ScriptsGrid.tsx | 22 ++-- src/app/_components/ServerForm.tsx | 10 +- src/app/_components/ServerList.tsx | 12 +-- src/app/_components/SettingsModal.tsx | 10 +- src/app/_components/SetupModal.tsx | 4 +- src/app/_components/TextViewer.tsx | 2 +- src/app/_components/ThemeProvider.tsx | 63 +++++++++++ src/app/_components/ThemeToggle.tsx | 39 +++++++ src/app/_components/ui/button.tsx | 20 ++-- src/app/_components/ui/toggle.tsx | 4 +- src/app/layout.tsx | 29 ++--- src/app/page.tsx | 14 ++- src/styles/globals.css | 34 +++++- 34 files changed, 395 insertions(+), 231 deletions(-) create mode 100644 src/app/_components/ThemeProvider.tsx create mode 100644 src/app/_components/ThemeToggle.tsx diff --git a/src/app/_components/AuthGuard.tsx b/src/app/_components/AuthGuard.tsx index 2b5fe08..4a0aacf 100644 --- a/src/app/_components/AuthGuard.tsx +++ b/src/app/_components/AuthGuard.tsx @@ -51,7 +51,7 @@ export function AuthGuard({ children }: AuthGuardProps) { return (
-
+

Loading...

diff --git a/src/app/_components/AuthModal.tsx b/src/app/_components/AuthModal.tsx index e26cb59..cb56602 100644 --- a/src/app/_components/AuthModal.tsx +++ b/src/app/_components/AuthModal.tsx @@ -39,7 +39,7 @@ export function AuthModal({ isOpen }: AuthModalProps) { {/* Header */}
- +

Authentication Required

@@ -90,7 +90,7 @@ export function AuthModal({ isOpen }: AuthModalProps) { {error && ( -
+
{error}
diff --git a/src/app/_components/Badge.tsx b/src/app/_components/Badge.tsx index 8fcd68e..cbf2ff5 100644 --- a/src/app/_components/Badge.tsx +++ b/src/app/_components/Badge.tsx @@ -18,11 +18,11 @@ export function Badge({ variant, type, noteType, status, executionMode, children case 'ct': return 'bg-primary/10 text-primary border-primary/20'; case 'addon': - return 'bg-purple-500/10 text-purple-400 border-purple-500/20'; + return 'bg-primary/10 text-primary border-primary/20'; case 'vm': - return 'bg-green-500/10 text-green-400 border-green-500/20'; + return 'bg-success/10 text-success border-success/20'; case 'pve': - return 'bg-orange-500/10 text-orange-400 border-orange-500/20'; + return 'bg-warning/10 text-warning border-warning/20'; default: return 'bg-muted text-muted-foreground border-border'; } @@ -34,7 +34,7 @@ export function Badge({ variant, type, noteType, status, executionMode, children return `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${type ? getTypeStyles(type) : getTypeStyles('unknown')}`; case 'updateable': - return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20'; + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success border border-success/20'; case 'privileged': return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20'; @@ -42,11 +42,11 @@ export function Badge({ variant, type, noteType, status, executionMode, children case 'status': switch (status) { case 'success': - return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/10 text-green-400 border border-green-500/20'; + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success/10 text-success border border-success/20'; case 'failed': - return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20'; + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-error/10 text-error border border-error/20'; case 'in_progress': - return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-500/10 text-yellow-400 border border-yellow-500/20'; + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning border border-warning/20'; default: return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border'; } @@ -56,7 +56,7 @@ export function Badge({ variant, type, noteType, status, executionMode, children case 'local': return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/10 text-primary border border-primary/20'; case 'ssh': - return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-500/10 text-purple-400 border border-purple-500/20'; + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary/10 text-primary border border-primary/20'; default: return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground border border-border'; } @@ -64,7 +64,7 @@ export function Badge({ variant, type, noteType, status, executionMode, children case 'note': switch (noteType) { case 'warning': - return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-500/10 text-yellow-400 border border-yellow-500/20'; + return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning border border-warning/20'; case 'error': return 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-destructive/10 text-destructive border border-destructive/20'; default: diff --git a/src/app/_components/CategorySidebar.tsx b/src/app/_components/CategorySidebar.tsx index ed3883c..6786f98 100644 --- a/src/app/_components/CategorySidebar.tsx +++ b/src/app/_components/CategorySidebar.tsx @@ -212,7 +212,7 @@ export function CategorySidebar({ )} {/* Tooltip */} -
+
{category} ({count})
diff --git a/src/app/_components/ConfirmationModal.tsx b/src/app/_components/ConfirmationModal.tsx index 5314ced..739a289 100644 --- a/src/app/_components/ConfirmationModal.tsx +++ b/src/app/_components/ConfirmationModal.tsx @@ -53,9 +53,9 @@ export function ConfirmationModal({
{isDanger ? ( - + ) : ( - + )}

{title}

diff --git a/src/app/_components/DiffViewer.tsx b/src/app/_components/DiffViewer.tsx index 9123ac4..48a5b61 100644 --- a/src/app/_components/DiffViewer.tsx +++ b/src/app/_components/DiffViewer.tsx @@ -45,7 +45,7 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer key={index} className={`flex font-mono text-sm ${ isAdded - ? 'bg-green-500/10 text-green-400 border-l-4 border-green-500' + ? 'bg-success/10 text-success border-l-4 border-success' : isRemoved ? 'bg-destructive/10 text-destructive border-l-4 border-destructive' : 'bg-muted text-muted-foreground' @@ -55,7 +55,7 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer {lineNumber}
- + {isAdded ? '+' : isRemoved ? '-' : ' '} {content} @@ -99,8 +99,8 @@ export function DiffViewer({ scriptSlug, filePath, isOpen, onClose }: DiffViewer
-
- Added (Remote) +
+ Added (Remote)
diff --git a/src/app/_components/DownloadedScriptsTab.tsx b/src/app/_components/DownloadedScriptsTab.tsx index a0d263a..fcaca50 100644 --- a/src/app/_components/DownloadedScriptsTab.tsx +++ b/src/app/_components/DownloadedScriptsTab.tsx @@ -355,7 +355,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr if (githubLoading || localLoading) { return (
-
+
Loading downloaded scripts...
); @@ -364,7 +364,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr if (githubError || localError) { return (
-
+
diff --git a/src/app/_components/ErrorModal.tsx b/src/app/_components/ErrorModal.tsx index f0a82d4..db79384 100644 --- a/src/app/_components/ErrorModal.tsx +++ b/src/app/_components/ErrorModal.tsx @@ -40,9 +40,9 @@ export function ErrorModal({
{type === 'success' ? ( - + ) : ( - + )}

{title}

@@ -54,20 +54,20 @@ export function ErrorModal({ {details && (

{type === 'success' ? 'Details:' : 'Error Details:'}

                 {details}
               
diff --git a/src/app/_components/ExecutionModeModal.tsx b/src/app/_components/ExecutionModeModal.tsx index cd806cb..0e94ebb 100644 --- a/src/app/_components/ExecutionModeModal.tsx +++ b/src/app/_components/ExecutionModeModal.tsx @@ -149,7 +149,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
-
+

@@ -216,7 +216,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E disabled={!selectedServer} variant="default" size="default" - className={!selectedServer ? 'bg-gray-400 cursor-not-allowed' : ''} + className={!selectedServer ? 'bg-muted-foreground cursor-not-allowed' : ''} > Run on Server diff --git a/src/app/_components/FilterBar.tsx b/src/app/_components/FilterBar.tsx index a48696d..6b10f07 100644 --- a/src/app/_components/FilterBar.tsx +++ b/src/app/_components/FilterBar.tsx @@ -171,7 +171,7 @@ export function FilterBar({ filters.showUpdatable === null ? "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground" : filters.showUpdatable === true - ? "border border-green-500/20 bg-green-500/10 text-green-400" + ? "border border-success/20 bg-success/10 text-success" : "border border-destructive/20 bg-destructive/10 text-destructive" }`} > @@ -388,7 +388,7 @@ export function FilterBar({ {filteredCount} of {totalScripts} scripts{" "} {hasActiveFilters && ( - + (filtered) )} @@ -398,7 +398,7 @@ export function FilterBar({ {/* Filter Persistence Status */} {!isLoadingFilters && saveFiltersEnabled && ( -

+
@@ -412,7 +412,7 @@ export function FilterBar({ onClick={clearAllFilters} variant="ghost" size="sm" - className="flex items-center space-x-1 text-red-600 hover:bg-red-50 hover:text-red-800 w-full sm:w-auto justify-center sm:justify-start" + className="flex items-center space-x-1 text-error hover:bg-error/10 hover:text-error-foreground w-full sm:w-auto justify-center sm:justify-start" > ('general'); const [githubToken, setGithubToken] = useState(''); const [saveFilter, setSaveFilter] = useState(false); @@ -298,7 +300,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
{/* Tabs */} -
+
+
+
+

Save Filters

Save your configured script filters.

@@ -379,7 +410,7 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr onClick={clearSavedFilters} variant="outline" size="sm" - className="text-red-600 hover:text-red-800" + className="text-error hover:text-error/80" > Clear @@ -433,8 +464,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr {message && (
{message.text}
@@ -561,8 +592,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr {message && (
{message.text}
diff --git a/src/app/_components/HelpModal.tsx b/src/app/_components/HelpModal.tsx index 6c096e9..e234f5d 100644 --- a/src/app/_components/HelpModal.tsx +++ b/src/app/_components/HelpModal.tsx @@ -57,9 +57,9 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
  • Password: Use username and password authentication
  • SSH Key: Use SSH key pair for secure authentication
  • -
    -
    SSH Key Features:
    -
      +
      +
      SSH Key Features:
      +
      • Generate Key Pair: Create new SSH keys automatically
      • View Public Key: Copy public key for server setup
      • Persistent Storage: Keys are stored securely on disk
      • @@ -344,7 +344,7 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'
      -
      +

      Web UI Access

      Automatically detect and access Web UI interfaces for your installed scripts. @@ -357,8 +357,8 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'

    • Port Detection: Uses script metadata to get correct port (e.g., actualbudget:5006)
    • Editable Fields: Manually edit IP and port values as needed
    -
    -

    💡 How it works:

    +
    +

    💡 How it works:

    • • Scripts automatically detect URLs like http://10.10.10.1:3000 during installation
    • • Re-detect button runs hostname -I inside the container via SSH
    • @@ -586,9 +586,9 @@ export function HelpModal({ isOpen, onClose, initialSection = 'server-settings'

      To save configuration changes, you must type the container ID exactly as shown to confirm your changes.

      -
      -
      ⚠️ Important Warnings
      -
        +
        +
        ⚠️ Important Warnings
        +
        • • Modifying LXC configuration can break your container
        • • Some changes may require container restart to take effect
        • • Always backup your configuration before making changes
        • diff --git a/src/app/_components/InstalledScriptsTab.tsx b/src/app/_components/InstalledScriptsTab.tsx index cb1cd5e..882feda 100644 --- a/src/app/_components/InstalledScriptsTab.tsx +++ b/src/app/_components/InstalledScriptsTab.tsx @@ -897,21 +897,21 @@ export function InstalledScriptsTab() { {stats && (
          -
          -
          {stats.total}
          -
          Total Installations
          +
          +
          {stats.total}
          +
          Total Installations
          -
          -
          {stats.byStatus.success}
          -
          Successful
          +
          +
          {stats.byStatus.success}
          +
          Successful
          -
          -
          {stats.byStatus.failed}
          -
          Failed
          +
          +
          {stats.byStatus.failed}
          +
          Failed
          -
          -
          {stats.byStatus.in_progress}
          -
          In Progress
          +
          +
          {stats.byStatus.in_progress}
          +
          In Progress
          )} @@ -1018,17 +1018,17 @@ export function InstalledScriptsTab() { {autoDetectStatus.type && (
          {autoDetectStatus.type === 'success' ? ( - + ) : ( - + )} @@ -1036,8 +1036,8 @@ export function InstalledScriptsTab() {

          {autoDetectStatus.message}

          @@ -1050,17 +1050,17 @@ export function InstalledScriptsTab() { {cleanupStatus.type && (
          {cleanupStatus.type === 'success' ? ( - + ) : ( - + )} @@ -1068,8 +1068,8 @@ export function InstalledScriptsTab() {

          {cleanupStatus.message}

          @@ -1085,18 +1085,18 @@ export function InstalledScriptsTab() {

          Auto-Detect LXC Containers (Must contain a tag with "community-script")

          -
          +
          - +
          -

          +

          How it works

          -
          +

          This feature will:

          • Connect to the selected server via SSH
          • @@ -1348,14 +1348,14 @@ export function InstalledScriptsTab() { {script.container_status && (
            {script.container_status === 'running' ? 'Running' : script.container_status === 'stopped' ? 'Stopped' : @@ -1397,7 +1397,7 @@ export function InstalledScriptsTab() { {containerStatuses.get(script.id) === 'running' && ( - + {script.container_id && ( handleUpdateScript(script)} disabled={containerStatuses.get(script.id) === 'stopped'} - className="text-cyan-300 hover:text-cyan-200 hover:bg-cyan-900/20 focus:bg-cyan-900/20" + className="text-info hover:text-info-foreground hover:bg-info/20 focus:bg-info/20" > Update @@ -1494,7 +1494,7 @@ export function InstalledScriptsTab() { handleOpenShell(script)} disabled={containerStatuses.get(script.id) === 'stopped'} - className="text-gray-300 hover:text-gray-200 hover:bg-gray-800/20 focus:bg-gray-800/20" + className="text-muted-foreground hover:text-foreground hover:bg-muted/20 focus:bg-muted/20" > Shell @@ -1503,7 +1503,7 @@ export function InstalledScriptsTab() { handleOpenWebUI(script)} disabled={containerStatuses.get(script.id) === 'stopped'} - className="text-blue-300 hover:text-blue-200 hover:bg-blue-900/20 focus:bg-blue-900/20" + className="text-info hover:text-info-foreground hover:bg-info/20 focus:bg-info/20" > Open UI @@ -1512,28 +1512,28 @@ export function InstalledScriptsTab() { handleAutoDetectWebUI(script)} disabled={autoDetectWebUIMutation.isPending ?? containerStatuses.get(script.id) === 'stopped'} - className="text-blue-300 hover:text-blue-200 hover:bg-blue-900/20 focus:bg-blue-900/20" + className="text-info hover:text-info-foreground hover:bg-info/20 focus:bg-info/20" > {autoDetectWebUIMutation.isPending ? 'Re-detect...' : 'Re-detect IP/Port'} )} {script.container_id && script.execution_mode === 'ssh' && ( <> - + handleLXCSettings(script)} - className="text-purple-300 hover:text-purple-200 hover:bg-purple-900/20 focus:bg-purple-900/20" + className="text-primary hover:text-primary-foreground hover:bg-primary/20 focus:bg-primary/20" > LXC Settings - + handleStartStop(script, (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'stop' : 'start')} disabled={controllingScriptId === script.id || (containerStatuses.get(script.id) ?? 'unknown') === 'unknown'} className={(containerStatuses.get(script.id) ?? 'unknown') === 'running' - ? "text-red-300 hover:text-red-200 hover:bg-red-900/20 focus:bg-red-900/20" - : "text-green-300 hover:text-green-200 hover:bg-green-900/20 focus:bg-green-900/20" + ? "text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" + : "text-success hover:text-success-foreground hover:bg-success/20 focus:bg-success/20" } > {controllingScriptId === script.id ? 'Working...' : (containerStatuses.get(script.id) ?? 'unknown') === 'running' ? 'Stop' : 'Start'} @@ -1541,7 +1541,7 @@ export function InstalledScriptsTab() { handleDestroy(script)} disabled={controllingScriptId === script.id} - className="text-red-300 hover:text-red-200 hover:bg-red-900/20 focus:bg-red-900/20" + className="text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" > {controllingScriptId === script.id ? 'Working...' : 'Destroy'} @@ -1549,11 +1549,11 @@ export function InstalledScriptsTab() { )} {(!script.container_id || script.execution_mode !== 'ssh') && ( <> - + handleDeleteScript(Number(script.id))} disabled={deleteScriptMutation.isPending} - className="text-red-300 hover:text-red-200 hover:bg-red-900/20 focus:bg-red-900/20" + className="text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" > {deleteScriptMutation.isPending ? 'Deleting...' : 'Delete'} diff --git a/src/app/_components/LXCSettingsModal.tsx b/src/app/_components/LXCSettingsModal.tsx index 5d1b6f4..f68c432 100644 --- a/src/app/_components/LXCSettingsModal.tsx +++ b/src/app/_components/LXCSettingsModal.tsx @@ -287,14 +287,14 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting {/* Warning Banner */} {configData?.has_changes && ( -
            +
            - +
            -

            +

            Configuration Mismatch Detected

            -

            +

            The cached configuration differs from the server. Click "Sync from Server" to get the latest version.

            @@ -305,16 +305,16 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting {/* Error Message */} {error && ( -
            +
            - +
            -

            Error

            -

            {error}

            +

            Error

            +

            {error}

            @@ -331,8 +331,8 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting onClick={() => setActiveTab('common')} className={`py-2 px-1 border-b-2 font-medium text-sm ${ activeTab === 'common' - ? 'border-blue-500 text-blue-600' - : 'border-transparent text-muted-foreground hover:text-foreground hover:border-gray-300' + ? 'border-primary text-primary' + : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' }`} > Common Settings @@ -341,8 +341,8 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting onClick={() => setActiveTab('advanced')} className={`py-2 px-1 border-b-2 font-medium text-sm ${ activeTab === 'advanced' - ? 'border-blue-500 text-blue-600' - : 'border-transparent text-muted-foreground hover:text-foreground hover:border-gray-300' + ? 'border-primary text-primary' + : 'border-transparent text-muted-foreground hover:text-foreground hover:border-border' }`} > Advanced Settings @@ -423,7 +423,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting id="onboot" checked={formData.onboot} onChange={(e) => handleInputChange('onboot', e.target.checked)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
            @@ -433,7 +433,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting id="unprivileged" checked={formData.unprivileged} onChange={(e) => handleInputChange('unprivileged', e.target.checked)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
            @@ -568,7 +568,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting id="feature_keyctl" checked={formData.feature_keyctl} onChange={(e) => handleInputChange('feature_keyctl', e.target.checked)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
            @@ -578,7 +578,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting id="feature_nesting" checked={formData.feature_nesting} onChange={(e) => handleInputChange('feature_nesting', e.target.checked)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
            @@ -588,7 +588,7 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting id="feature_fuse" checked={formData.feature_fuse} onChange={(e) => handleInputChange('feature_fuse', e.target.checked)} - className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" + className="h-4 w-4 text-primary focus:ring-primary border-border rounded" />
            @@ -696,9 +696,9 @@ export function LXCSettingsModal({ isOpen, script, onClose, onSave }: LXCSetting
            {resultType === 'success' ? ( - + ) : ( - + )}

            {resultType === 'success' ? 'Success' : 'Error'} diff --git a/src/app/_components/PublicKeyModal.tsx b/src/app/_components/PublicKeyModal.tsx index 2afc80d..980100b 100644 --- a/src/app/_components/PublicKeyModal.tsx +++ b/src/app/_components/PublicKeyModal.tsx @@ -97,8 +97,8 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI {/* Header */}
            -
            - +
            +

            SSH Public Key

            diff --git a/src/app/_components/ReleaseNotesModal.tsx b/src/app/_components/ReleaseNotesModal.tsx index c684dbb..3170967 100644 --- a/src/app/_components/ReleaseNotesModal.tsx +++ b/src/app/_components/ReleaseNotesModal.tsx @@ -67,7 +67,7 @@ export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: Release {/* Header */}
            - +

            Release Notes

            - + {script.container_id && ( Update @@ -311,7 +311,7 @@ export function ScriptInstallationCard({ Shell @@ -320,20 +320,20 @@ export function ScriptInstallationCard({ Open UI )} {script.container_id && script.execution_mode === 'ssh' && ( <> - + onStartStop(containerStatus === 'running' ? 'stop' : 'start')} disabled={isControlling || containerStatus === 'unknown'} className={containerStatus === 'running' - ? "text-red-300 hover:text-red-200 hover:bg-red-900/20 focus:bg-red-900/20" - : "text-green-300 hover:text-green-200 hover:bg-green-900/20 focus:bg-green-900/20" + ? "text-error hover:text-error-foreground hover:bg-error/20 focus:bg-error/20" + : "text-success hover:text-success-foreground hover:bg-success/20 focus:bg-success/20" } > {isControlling ? 'Working...' : containerStatus === 'running' ? 'Stop' : 'Start'} @@ -341,7 +341,7 @@ export function ScriptInstallationCard({ {isControlling ? 'Working...' : 'Destroy'} @@ -349,11 +349,11 @@ export function ScriptInstallationCard({ )} {(!script.container_id || script.execution_mode !== 'ssh') && ( <> - + {isDeleting ? 'Deleting...' : 'Delete'} diff --git a/src/app/_components/ScriptsGrid.tsx b/src/app/_components/ScriptsGrid.tsx index 8182b55..6ff8142 100644 --- a/src/app/_components/ScriptsGrid.tsx +++ b/src/app/_components/ScriptsGrid.tsx @@ -573,7 +573,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { if (githubLoading || localLoading) { return (
            -
            +
            Loading scripts...
            ); @@ -582,7 +582,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { if (githubError || localError) { return (
            -
            +
            @@ -684,7 +684,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { onToggleSelect={toggleScriptSelection} /> {/* NEW badge */} -
            +
            NEW
            @@ -705,7 +705,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { disabled={loadSingleScriptMutation.isPending} variant="outline" size="sm" - className="bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30 text-blue-300 hover:text-blue-200 hover:border-blue-400/50" + className="bg-info/10 hover:bg-info/20 border-info/30 text-info hover:text-info-foreground hover:border-info/50" > {loadSingleScriptMutation.isPending ? ( <> @@ -791,7 +791,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) {
            0 ? 'bg-yellow-500' : 'bg-primary' + downloadProgress.failed.length > 0 ? 'bg-warning' : 'bg-primary' }`} style={{ width: `${(downloadProgress.current / downloadProgress.total) * 100}%` }} /> @@ -811,9 +811,9 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { key={i} className={`px-1 py-0.5 rounded text-xs ${ isCompleted - ? isFailed ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' : 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' + ? isFailed ? 'bg-error/10 text-error' : 'bg-success/10 text-success' : isCurrent - ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 animate-pulse' + ? 'bg-info/10 text-info animate-pulse' : 'bg-muted text-muted-foreground' }`} > @@ -826,18 +826,18 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { {/* Failed Scripts Details */} {downloadProgress.failed.length > 0 && ( -
            +
            - + - + Failed Downloads ({downloadProgress.failed.length})
            {downloadProgress.failed.map((failed, index) => ( -
            +
            {failed.slug}: {failed.error}
            ))} diff --git a/src/app/_components/ServerForm.tsx b/src/app/_components/ServerForm.tsx index 786b10f..33a80ec 100644 --- a/src/app/_components/ServerForm.tsx +++ b/src/app/_components/ServerForm.tsx @@ -350,13 +350,13 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel {/* Show generated key status */} {formData.key_generated && ( -
            +
            - + - + SSH key pair generated successfully
            @@ -365,13 +365,13 @@ export function ServerForm({ onSubmit, initialData, isEditing = false, onCancel variant="outline" size="sm" onClick={() => setShowPublicKeyModal(true)} - className="gap-2 border-blue-500/20 text-blue-400 bg-blue-500/10 hover:bg-blue-500/20" + className="gap-2 border-info/20 text-info bg-info/10 hover:bg-info/20" > View Public Key
            -

            +

            The private key has been generated and will be saved with the server.

            diff --git a/src/app/_components/ServerList.tsx b/src/app/_components/ServerList.tsx index 4697501..7a1d6f3 100644 --- a/src/app/_components/ServerList.tsx +++ b/src/app/_components/ServerList.tsx @@ -169,8 +169,8 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) {
            -
            - +
            +
            @@ -202,8 +202,8 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) { {connectionResults.has(server.id) && (
            {connectionResults.get(server.id)?.success ? ( @@ -231,7 +231,7 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) { disabled={testingConnections.has(server.id)} variant="outline" size="sm" - className="w-full sm:w-auto border-green-500/20 text-green-400 bg-green-500/10 hover:bg-green-500/20" + className="w-full sm:w-auto border-success/20 text-success bg-success/10 hover:bg-success/20" > {testingConnections.has(server.id) ? ( <> @@ -258,7 +258,7 @@ export function ServerList({ servers, onUpdate, onDelete }: ServerListProps) { onClick={() => handleViewPublicKey(server)} variant="outline" size="sm" - className="flex-1 sm:flex-none border-blue-500/20 text-blue-400 bg-blue-500/10 hover:bg-blue-500/20" + className="flex-1 sm:flex-none border-info/20 text-info bg-info/10 hover:bg-info/20" > View Public Key diff --git a/src/app/_components/SettingsModal.tsx b/src/app/_components/SettingsModal.tsx index 5457e9f..3aba9f4 100644 --- a/src/app/_components/SettingsModal.tsx +++ b/src/app/_components/SettingsModal.tsx @@ -130,13 +130,13 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
            - +
            -

            Error

            -
            {error}
            +

            Error

            +
            {error}
            @@ -152,8 +152,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {

            Saved Servers

            {loading ? (
            -
            -

            Loading servers...

            +
            +

            Loading servers...

            ) : (
            - +

            Setup Authentication

            @@ -180,7 +180,7 @@ export function SetupModal({ isOpen, onComplete }: SetupModalProps) {
            {error && ( -
            +
            {error}
            diff --git a/src/app/_components/TextViewer.tsx b/src/app/_components/TextViewer.tsx index 2ea10f3..9635f8d 100644 --- a/src/app/_components/TextViewer.tsx +++ b/src/app/_components/TextViewer.tsx @@ -185,7 +185,7 @@ export function TextViewer({ scriptName, isOpen, onClose }: TextViewerProps) { ) : (
            -
            +
            {activeTab === 'ct' ? 'CT script not found' : 'Install script not found'}
            diff --git a/src/app/_components/ThemeProvider.tsx b/src/app/_components/ThemeProvider.tsx new file mode 100644 index 0000000..43ffadc --- /dev/null +++ b/src/app/_components/ThemeProvider.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { createContext, useContext, useEffect, useState } from 'react'; + +type Theme = 'light' | 'dark'; + +interface ThemeContextType { + theme: Theme; + setTheme: (theme: Theme) => void; +} + +const ThemeContext = createContext(undefined); + +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} + +interface ThemeProviderProps { + children: React.ReactNode; +} + +export function ThemeProvider({ children }: ThemeProviderProps) { + const [theme, setThemeState] = useState('dark'); // Default to dark mode + const [mounted, setMounted] = useState(false); + + // Load theme from localStorage on mount + useEffect(() => { + const savedTheme = localStorage.getItem('theme') as Theme; + if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) { + setThemeState(savedTheme); + } + setMounted(true); + }, []); + + // Apply theme to document element + useEffect(() => { + if (mounted) { + const root = document.documentElement; + root.classList.remove('light', 'dark'); + root.classList.add(theme); + } + }, [theme, mounted]); + + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + localStorage.setItem('theme', newTheme); + }; + + // Prevent hydration mismatch by not rendering until mounted + if (!mounted) { + return
            {children}
            ; + } + + return ( + + {children} + + ); +} diff --git a/src/app/_components/ThemeToggle.tsx b/src/app/_components/ThemeToggle.tsx new file mode 100644 index 0000000..1c04fbd --- /dev/null +++ b/src/app/_components/ThemeToggle.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { Sun, Moon } from 'lucide-react'; +import { useTheme } from './ThemeProvider'; +import { Button } from './ui/button'; + +interface ThemeToggleProps { + className?: string; + showLabel?: boolean; +} + +export function ThemeToggle({ className = '', showLabel = false }: ThemeToggleProps) { + const { theme, setTheme } = useTheme(); + + const toggleTheme = () => { + setTheme(theme === 'light' ? 'dark' : 'light'); + }; + + return ( + + ); +} diff --git a/src/app/_components/ui/button.tsx b/src/app/_components/ui/button.tsx index 78b54f4..1a5971e 100644 --- a/src/app/_components/ui/button.tsx +++ b/src/app/_components/ui/button.tsx @@ -34,16 +34,16 @@ const buttonVariants = cva( "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300", linkHover2: "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300", - // Dark theme action button variants - edit: "bg-blue-900/20 hover:bg-blue-900/30 border border-blue-700/50 text-blue-300 hover:text-blue-200 hover:border-blue-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md", - update: "bg-cyan-900/20 hover:bg-cyan-900/30 border border-cyan-700/50 text-cyan-300 hover:text-cyan-200 hover:border-cyan-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md", - shell: "bg-gray-800/20 hover:bg-gray-800/30 border border-gray-600/50 text-gray-300 hover:text-gray-200 hover:border-gray-500/60 transition-all duration-200 hover:scale-105 hover:shadow-md", - openui: "bg-blue-900/20 hover:bg-blue-900/30 border border-blue-700/50 text-blue-300 hover:text-blue-200 hover:border-blue-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md", - start: "bg-green-900/20 hover:bg-green-900/30 border border-green-700/50 text-green-300 hover:text-green-200 hover:border-green-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md", - stop: "bg-red-900/20 hover:bg-red-900/30 border border-red-700/50 text-red-300 hover:text-red-200 hover:border-red-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md", - delete: "bg-red-900/20 hover:bg-red-900/30 border border-red-700/50 text-red-300 hover:text-red-200 hover:border-red-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md disabled:hover:scale-100", - save: "bg-green-900/20 hover:bg-green-900/30 border border-green-700/50 text-green-300 hover:text-green-200 hover:border-green-600/60 transition-all duration-200 hover:scale-105 hover:shadow-md disabled:hover:scale-100", - cancel: "bg-gray-800/20 hover:bg-gray-800/30 border border-gray-600/50 text-gray-300 hover:text-gray-200 hover:border-gray-500/60 transition-all duration-200 hover:scale-105 hover:shadow-md", + // Action button variants + edit: "bg-info/20 hover:bg-info/30 border border-info/50 text-info hover:text-info-foreground hover:border-info/60 transition-all duration-200 hover:scale-105 hover:shadow-md", + update: "bg-info/20 hover:bg-info/30 border border-info/50 text-info hover:text-info-foreground hover:border-info/60 transition-all duration-200 hover:scale-105 hover:shadow-md", + shell: "bg-muted/20 hover:bg-muted/30 border border-muted text-muted-foreground hover:text-foreground hover:border-muted-foreground transition-all duration-200 hover:scale-105 hover:shadow-md", + openui: "bg-info/20 hover:bg-info/30 border border-info/50 text-info hover:text-info-foreground hover:border-info/60 transition-all duration-200 hover:scale-105 hover:shadow-md", + start: "bg-success/20 hover:bg-success/30 border border-success/50 text-success hover:text-success-foreground hover:border-success/60 transition-all duration-200 hover:scale-105 hover:shadow-md", + stop: "bg-error/20 hover:bg-error/30 border border-error/50 text-error hover:text-error-foreground hover:border-error/60 transition-all duration-200 hover:scale-105 hover:shadow-md", + delete: "bg-error/20 hover:bg-error/30 border border-error/50 text-error hover:text-error-foreground hover:border-error/60 transition-all duration-200 hover:scale-105 hover:shadow-md disabled:hover:scale-100", + save: "bg-success/20 hover:bg-success/30 border border-success/50 text-success hover:text-success-foreground hover:border-success/60 transition-all duration-200 hover:scale-105 hover:shadow-md disabled:hover:scale-100", + cancel: "bg-muted/20 hover:bg-muted/30 border border-muted text-muted-foreground hover:text-foreground hover:border-muted-foreground transition-all duration-200 hover:scale-105 hover:shadow-md", }, size: { default: "h-10 px-4 py-2", diff --git a/src/app/_components/ui/toggle.tsx b/src/app/_components/ui/toggle.tsx index bd8d3f2..244d64f 100644 --- a/src/app/_components/ui/toggle.tsx +++ b/src/app/_components/ui/toggle.tsx @@ -22,8 +22,8 @@ const Toggle = React.forwardRef( {...props} />
            diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b52fa38..9d61099 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import { Geist } from "next/font/google"; import { TRPCReactProvider } from "~/trpc/react"; import { AuthProvider } from "./_components/AuthProvider"; import { AuthGuard } from "./_components/AuthGuard"; +import { ThemeProvider } from "./_components/ThemeProvider"; export const metadata: Metadata = { title: "PVE Scripts local", @@ -32,28 +33,20 @@ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - - -