diff --git a/src/app/api-keys/page.tsx b/src/app/api-keys/page.tsx
index 2593f3f..6f6221f 100644
--- a/src/app/api-keys/page.tsx
+++ b/src/app/api-keys/page.tsx
@@ -22,6 +22,7 @@ import {
RefreshCw,
Shield,
Trash2,
+ X,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -855,6 +856,19 @@ export default function ApiKeysPage() {
{showCreate && (
+
event.stopPropagation()}
+ >
+
+
+
event.stopPropagation()}
>
@@ -895,18 +908,6 @@ export default function ApiKeysPage() {
: "Create a new API key for scripts, bots, and CI/CD jobs."}
-
- ×
-
{generatedKey ? (
@@ -1195,6 +1196,7 @@ export default function ApiKeysPage() {
)}
+
)}
diff --git a/src/app/apps/installed/page.tsx b/src/app/apps/installed/page.tsx
index b2c6380..0bb5145 100644
--- a/src/app/apps/installed/page.tsx
+++ b/src/app/apps/installed/page.tsx
@@ -483,7 +483,16 @@ function InstallOperationsModal({
padding: 16,
}}
>
-
+
+
+
+
@@ -527,19 +536,6 @@ function InstallOperationsModal({
{install.containerName ? ` • ${install.containerName}` : ""}
-
-
-
-
-
)}
+
);
diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx
index 758b69b..2a4a9e2 100644
--- a/src/app/apps/page.tsx
+++ b/src/app/apps/page.tsx
@@ -1003,59 +1003,59 @@ function InstallModal({
return createPortal(
+
+
+
-
-
-
-
- {mode === "custom"
- ? "Custom App Install"
- : `Install ${template?.name ?? "App"}`}
-
-
- {mode === "custom"
- ? "Define your own Docker image, ports, env vars, volumes, and runtime options."
- : "Start from the template, then override ports, env vars, paths, or command as needed."}
-
-
-
-
-
-
-
+
+
+
+
+ {mode === "custom"
+ ? "Custom App Install"
+ : `Install ${template?.name ?? "App"}`}
+
+
+ {mode === "custom"
+ ? "Define your own Docker image, ports, env vars, volumes, and runtime options."
+ : "Start from the template, then override ports, env vars, paths, or command as needed."}
+
+
+
+
+
,
document.body,
diff --git a/src/app/backups/page.tsx b/src/app/backups/page.tsx
index 7a67cbe..489e9ce 100644
--- a/src/app/backups/page.tsx
+++ b/src/app/backups/page.tsx
@@ -259,6 +259,15 @@ function CreateBackupModal({
return (
+
+
+
+
@@ -287,17 +296,6 @@ function CreateBackupModal({
Backup a server's data to local or S3
-
-
-
{error && (
@@ -626,6 +624,7 @@ function CreateBackupModal({
+
);
@@ -919,6 +918,15 @@ export default function BackupsPage() {
/>
{downloadDialog && (
+
+
setDownloadDialog(null)}
+ className="modal-close"
+ aria-label="Close download backup modal"
+ >
+
+
@@ -952,17 +960,6 @@ export default function BackupsPage() {
Pilih format download untuk {downloadDialog.name}
-
setDownloadDialog(null)}
- style={{
- background: "none",
- border: "none",
- cursor: "pointer",
- color: "var(--text-muted)",
- }}
- >
-
-
+
)}
diff --git a/src/app/containers/components/modals/ContainerDetailsModal.tsx b/src/app/containers/components/modals/ContainerDetailsModal.tsx
index fe9b31c..e4925ea 100644
--- a/src/app/containers/components/modals/ContainerDetailsModal.tsx
+++ b/src/app/containers/components/modals/ContainerDetailsModal.tsx
@@ -58,6 +58,15 @@ export default function ContainerDetailsModal({
return (
+
+
+
+
@@ -103,18 +112,6 @@ export default function ContainerDetailsModal({
: ""}
-
-
-
) : null}
+
);
diff --git a/src/app/containers/components/modals/ContainerFileManagerModal.tsx b/src/app/containers/components/modals/ContainerFileManagerModal.tsx
index 9f35ebf..5db8a09 100644
--- a/src/app/containers/components/modals/ContainerFileManagerModal.tsx
+++ b/src/app/containers/components/modals/ContainerFileManagerModal.tsx
@@ -491,6 +491,18 @@ export default function ContainerFileManagerModal({
current?.onConfirm();
}}
/>
+
+
{
+ if (!(await confirmLoseChanges())) return;
+ onClose();
+ }}
+ className="modal-close"
+ aria-label="Close file manager modal"
+ >
+
+
-
{
- if (!(await confirmLoseChanges())) return;
- onClose();
- }}
- style={{
- background: "none",
- border: "none",
- cursor: "pointer",
- color: "var(--text-muted)",
- }}
- >
-
-
+
);
diff --git a/src/app/containers/components/modals/ContainerLogsModal.tsx b/src/app/containers/components/modals/ContainerLogsModal.tsx
index 8ebd096..757df3d 100644
--- a/src/app/containers/components/modals/ContainerLogsModal.tsx
+++ b/src/app/containers/components/modals/ContainerLogsModal.tsx
@@ -48,6 +48,15 @@ export default function ContainerLogsModal({
return (
+
+
+
+
@@ -80,18 +89,6 @@ export default function ContainerLogsModal({
Logs - {container.name}
-
-
-
+
);
diff --git a/src/app/containers/components/modals/DeployContainerModal.tsx b/src/app/containers/components/modals/DeployContainerModal.tsx
index 0719e11..bc35e78 100644
--- a/src/app/containers/components/modals/DeployContainerModal.tsx
+++ b/src/app/containers/components/modals/DeployContainerModal.tsx
@@ -989,9 +989,7 @@ export default function DeployContainerModal({
},
});
- void containersApi
- .cancelJob(jobId)
- .catch(() => undefined);
+ void containersApi.cancelJob(jobId).catch(() => undefined);
},
},
});
@@ -1079,6 +1077,15 @@ export default function DeployContainerModal({
return (
+
+
+
+
@@ -1140,27 +1147,6 @@ export default function DeployContainerModal({
: "Select a source to deploy from. You can choose from pre-built app templates, connect your Git provider, or manually enter deployment details."}
-
-
-
{error ? (
)}
+
);
diff --git a/src/app/databases/components/AddDatabaseModal.tsx b/src/app/databases/components/AddDatabaseModal.tsx
index fa16cd1..930dd19 100644
--- a/src/app/databases/components/AddDatabaseModal.tsx
+++ b/src/app/databases/components/AddDatabaseModal.tsx
@@ -467,16 +467,25 @@ export default function AddDatabaseModal({
return (
-
+
+
+
+
+
@@ -511,20 +520,9 @@ export default function AddDatabaseModal({
>
{choice === null
? "Select a source to deploy from"
- : "Deploy a new database container on your server"}
+ : "Deploy a new database container on your server"}
-
-
-
{error && (
@@ -1052,6 +1050,7 @@ export default function AddDatabaseModal({
)}
+
);
diff --git a/src/app/domains/components/AddDomainModal.tsx b/src/app/domains/components/AddDomainModal.tsx
index e11de23..0430502 100644
--- a/src/app/domains/components/AddDomainModal.tsx
+++ b/src/app/domains/components/AddDomainModal.tsx
@@ -478,6 +478,15 @@ export default function AddDomainModal({
return (
+
+
+
+
@@ -512,17 +521,6 @@ export default function AddDomainModal({
: "Configure domain with optional auto-SSL"}
-
-
-
{error && (
@@ -744,7 +742,7 @@ export default function AddDomainModal({
}}
>
Isolated config means the Nginx config for this domain will be
- created separately and won't be shared with any other domain.
+ created separately and won't be shared with any other domain.
This allows you to reuse target containers and ports that are
already in use by other domains, as long as those domains are
not using Shared mode on the same server.
@@ -803,6 +801,7 @@ export default function AddDomainModal({
+
);
diff --git a/src/app/firewall/page.tsx b/src/app/firewall/page.tsx
index c916e14..2b7a521 100644
--- a/src/app/firewall/page.tsx
+++ b/src/app/firewall/page.tsx
@@ -16,6 +16,7 @@ import {
ToggleRight,
Loader2,
Server as ServerIcon,
+ X,
} from "lucide-react";
import {
security as securityApi,
@@ -1043,16 +1044,28 @@ export default function FirewallPage() {
{showAddRule && (
setShowAddRule(false)}>
event.stopPropagation()}
+ >
+
setShowAddRule(false)}
+ className="modal-close"
+ aria-label="Close add firewall rule modal"
+ >
+
+
+
@@ -1069,12 +1082,6 @@ export default function FirewallPage() {
Configure a new UFW rule for this server.
-
setShowAddRule(false)}
- >
- Close
-
+
)}
diff --git a/src/app/git/components/GitProvidersPanel.tsx b/src/app/git/components/GitProvidersPanel.tsx
index 56fd007..5ce52da 100644
--- a/src/app/git/components/GitProvidersPanel.tsx
+++ b/src/app/git/components/GitProvidersPanel.tsx
@@ -492,8 +492,21 @@ export default function GitProvidersPanel({
const renderProviderSelectorModal = () => (
event.stopPropagation()}
+ >
+
+
+
+
@@ -531,30 +544,10 @@ export default function GitProvidersPanel({
and repository access.
-
-
-
{renderProviderSelectionRow()}
+
);
@@ -1221,8 +1214,27 @@ export default function GitProvidersPanel({
{draft ? (
event.stopPropagation()}
+ >
+
+
+
+
@@ -1265,24 +1277,6 @@ export default function GitProvidersPanel({
flow.
-
-
-
@@ -1341,6 +1335,7 @@ export default function GitProvidersPanel({
)}
+
) : null}
diff --git a/src/app/globals.css b/src/app/globals.css
index 302d6ff..3c5153a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -822,6 +822,7 @@ body {
contain: layout paint style;
}
.modal {
+ position: relative;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
@@ -838,6 +839,73 @@ body {
contain: content;
}
+.modal-shell {
+ position: relative;
+ width: 100%;
+ max-width: 480px;
+ --modal-close-size: 28px;
+}
+
+.modal-close {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 5;
+ display: inline-flex;
+ width: var(--modal-close-size);
+ height: var(--modal-close-size);
+ min-width: var(--modal-close-size);
+ flex: 0 0 var(--modal-close-size);
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ margin: 0;
+ border: 2px solid rgba(255, 255, 255, 0.82) !important;
+ border-radius: 50%;
+ background: #2c2c2c !important;
+ color: #ffffff !important;
+ /* box-shadow:
+ 0 14px 30px rgba(220, 38, 38, 0.34),
+ 0 4px 14px rgba(2, 6, 23, 0.26); */
+ cursor: pointer;
+ line-height: 1;
+ transition:
+ background 0.18s ease,
+ box-shadow 0.18s ease,
+ transform 0.18s ease;
+ transform: translate(50%, -50%);
+ transition: all 0.4s ease;
+}
+
+.modal-close:hover {
+ background: #b91c1c !important;
+ /* box-shadow:
+ 0 16px 36px rgba(220, 38, 38, 0.42),
+ 0 6px 18px rgba(2, 6, 23, 0.28); */
+ transform: translate(50%, calc(-50% - 1px)) rotate(180deg);
+}
+
+.modal-close:disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+ transform: translate(50%, -50%);
+}
+
+.modal-close:focus-visible {
+ outline: 3px solid rgba(239, 68, 68, 0.3);
+ outline-offset: 3px;
+}
+
+.modal-close svg {
+ stroke-width: 2.25;
+}
+
+@media (max-width: 640px) {
+ .modal-close {
+ --modal-close-size: 28px;
+ }
+}
+
.modal::-webkit-scrollbar {
width: 12px;
}
diff --git a/src/app/networks/page.tsx b/src/app/networks/page.tsx
index 7b57165..955562c 100644
--- a/src/app/networks/page.tsx
+++ b/src/app/networks/page.tsx
@@ -111,6 +111,15 @@ function AddNetworkModal({
return (
+
+
+
+
@@ -140,17 +149,6 @@ function AddNetworkModal({
database.
-
-
-
{error && (
@@ -356,6 +354,7 @@ function AddNetworkModal({
+
);
@@ -439,7 +438,16 @@ function NetworkDetailsModal({
padding: 16,
}}
>
-
+
+
+
+
@@ -495,17 +503,6 @@ function NetworkDetailsModal({
: "Unknown server"}
-
-
-
) : null}
+
);
diff --git a/src/app/notifications/components/NotificationsSettingsPanel.tsx b/src/app/notifications/components/NotificationsSettingsPanel.tsx
index 1fe30e1..8851835 100644
--- a/src/app/notifications/components/NotificationsSettingsPanel.tsx
+++ b/src/app/notifications/components/NotificationsSettingsPanel.tsx
@@ -596,6 +596,19 @@ export default function NotificationsSettingsPanel({
{draft ? (
+
+
+
+
@@ -637,24 +650,6 @@ export default function NotificationsSettingsPanel({
: "Set up a new notification provider and configure which events will trigger notifications."}
-
-
-
@@ -1424,6 +1419,7 @@ export default function NotificationsSettingsPanel({
+
) : null}
diff --git a/src/app/projects/[projectId]/environments/[environmentId]/components/import/ImportFromSyncModal.tsx b/src/app/projects/[projectId]/environments/[environmentId]/components/import/ImportFromSyncModal.tsx
index ddb599c..e2c7ff9 100644
--- a/src/app/projects/[projectId]/environments/[environmentId]/components/import/ImportFromSyncModal.tsx
+++ b/src/app/projects/[projectId]/environments/[environmentId]/components/import/ImportFromSyncModal.tsx
@@ -126,7 +126,11 @@ export default function ImportFromSyncModal({
);
setServerContainers((current) =>
current.map((item) =>
- item.id === container.id ? { ...item, environmentId } : item,
+ item.id === container.id
+ ? { ...item, environmentId }
+ : item.environmentId === environmentId
+ ? item
+ : item,
),
);
} catch (assignError) {
@@ -143,17 +147,29 @@ export default function ImportFromSyncModal({
return (
event.stopPropagation()}
+ >
+
+
+
+
@@ -178,18 +194,6 @@ export default function ImportFromSyncModal({
{environmentName}.
-
-
-
- Services / Containers
+
+ Services / Containers
+
void handleAssign(container)}
disabled={
- assigningContainerId !== null || syncing || loading
+ assigningContainerId !== null ||
+ syncing ||
+ loading
}
style={{
color: assignedElsewhere ? "#f59e0b" : "#93c5fd",
@@ -457,6 +465,7 @@ export default function ImportFromSyncModal({
{error}
) : null}
+
);
diff --git a/src/app/projects/components/EnvironmentFormModal.tsx b/src/app/projects/components/EnvironmentFormModal.tsx
index 4c6f0dd..740ac74 100644
--- a/src/app/projects/components/EnvironmentFormModal.tsx
+++ b/src/app/projects/components/EnvironmentFormModal.tsx
@@ -61,9 +61,22 @@ export default function EnvironmentFormModal({
return (
+
event.stopPropagation()}
+ >
+
+
+
@@ -250,6 +250,7 @@ export default function EnvironmentFormModal({
+
);
}
diff --git a/src/app/projects/components/EnvironmentOpsModal.tsx b/src/app/projects/components/EnvironmentOpsModal.tsx
index b9e3805..b04e273 100644
--- a/src/app/projects/components/EnvironmentOpsModal.tsx
+++ b/src/app/projects/components/EnvironmentOpsModal.tsx
@@ -79,9 +79,22 @@ export default function EnvironmentOpsModal({
return (
+
event.stopPropagation()}
+ >
+
+
+
+
);
}
diff --git a/src/app/projects/components/ProjectFormModal.tsx b/src/app/projects/components/ProjectFormModal.tsx
index f25709d..6e1a38a 100644
--- a/src/app/projects/components/ProjectFormModal.tsx
+++ b/src/app/projects/components/ProjectFormModal.tsx
@@ -44,23 +44,36 @@ export default function ProjectFormModal({
return (
);
}
diff --git a/src/app/s3-storage/components/S3StorageSettingsPanel.tsx b/src/app/s3-storage/components/S3StorageSettingsPanel.tsx
index 84d5aac..2483624 100644
--- a/src/app/s3-storage/components/S3StorageSettingsPanel.tsx
+++ b/src/app/s3-storage/components/S3StorageSettingsPanel.tsx
@@ -599,6 +599,19 @@ export default function S3StorageSettingsPanel({
{draft ? (
+
+
+
+
@@ -639,24 +652,6 @@ export default function S3StorageSettingsPanel({
use it.
-
-
-
@@ -1148,6 +1143,7 @@ export default function S3StorageSettingsPanel({
+
) : null}
diff --git a/src/app/security/components/AddFirewallRuleModal.tsx b/src/app/security/components/AddFirewallRuleModal.tsx
index 3c85cd7..fd0a175 100644
--- a/src/app/security/components/AddFirewallRuleModal.tsx
+++ b/src/app/security/components/AddFirewallRuleModal.tsx
@@ -1,4 +1,4 @@
-import { Loader2, Plus } from "lucide-react";
+import { Loader2, Plus, X } from "lucide-react";
import type { RuleFormState } from "./security-types";
interface AddFirewallRuleModalProps {
@@ -19,16 +19,28 @@ export default function AddFirewallRuleModal({
return (
event.stopPropagation()}
+ >
+
+
+
+
Add Firewall Rule
-
- x
-
@@ -174,6 +173,7 @@ export default function AddFirewallRuleModal({
+
);
diff --git a/src/app/servers/components/ServerConfigModal.tsx b/src/app/servers/components/ServerConfigModal.tsx
index ea25403..9505f4c 100644
--- a/src/app/servers/components/ServerConfigModal.tsx
+++ b/src/app/servers/components/ServerConfigModal.tsx
@@ -628,6 +628,15 @@ export default function ServerConfigModal({
return (
+
+
+
+
-
+
void loadSnapshot()}
@@ -954,17 +963,6 @@ export default function ServerConfigModal({
>
Refresh
-
-
-
@@ -1156,6 +1154,7 @@ export default function ServerConfigModal({
) : (
renderActiveTab()
)}
+
);
diff --git a/src/app/servers/components/ServerFormModal.tsx b/src/app/servers/components/ServerFormModal.tsx
index b36ba49..04f80d5 100644
--- a/src/app/servers/components/ServerFormModal.tsx
+++ b/src/app/servers/components/ServerFormModal.tsx
@@ -92,20 +92,29 @@ export default function ServerFormModal({
return (
-
+
+
+
+
+
{isEdit ? "Edit Server" : "Add Server"}
-
-
-
{error && (
+
);
diff --git a/src/app/servers/page.tsx b/src/app/servers/page.tsx
index 6b602f8..0f68177 100644
--- a/src/app/servers/page.tsx
+++ b/src/app/servers/page.tsx
@@ -458,22 +458,39 @@ export default function ServersPage() {
onClick={closeDeleteModal}
>
event.stopPropagation()}
>
+ {!confirmDeleteStep ? (
+
+
+
+ ) : null}
+
{!confirmDeleteStep ? (
@@ -515,20 +532,6 @@ export default function ServersPage() {
server access assignments.
-
-
-
)}
+
) : null}
diff --git a/src/app/ssl/components/IssueCertificateModal.tsx b/src/app/ssl/components/IssueCertificateModal.tsx
index 0a24b9b..d30588f 100644
--- a/src/app/ssl/components/IssueCertificateModal.tsx
+++ b/src/app/ssl/components/IssueCertificateModal.tsx
@@ -40,6 +40,15 @@ export default function IssueCertificateModal({
return (
+
+
+
+
@@ -68,18 +77,6 @@ export default function IssueCertificateModal({
Create a new SSL record from an existing domain.
-
-
-
{error ? (
@@ -188,6 +185,7 @@ export default function IssueCertificateModal({
+
);
diff --git a/src/app/terminal/page.tsx b/src/app/terminal/page.tsx
index 18a52e3..6f9453f 100644
--- a/src/app/terminal/page.tsx
+++ b/src/app/terminal/page.tsx
@@ -771,17 +771,29 @@ function TerminalPageContent() {
onClick={() => setShowPicker(false)}
style={{ zIndex: 200 }}
>
+ event.stopPropagation()}
+ >
+
setShowPicker(false)}
+ className="modal-close"
+ aria-label="Close terminal session picker"
+ >
+
+
event.stopPropagation()}
>
@@ -804,17 +816,6 @@ function TerminalPageContent() {
Choose a server to open or restore an SSH session.
-
setShowPicker(false)}
- style={{
- background: "none",
- border: "none",
- color: "var(--text-muted)",
- cursor: "pointer",
- }}
- >
-
-
{pickerError && (
@@ -927,6 +928,7 @@ function TerminalPageContent() {
)}
+
)}
diff --git a/src/app/users/components/EditRoleModal.tsx b/src/app/users/components/EditRoleModal.tsx
index 1afa16d..3314728 100644
--- a/src/app/users/components/EditRoleModal.tsx
+++ b/src/app/users/components/EditRoleModal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { AlertCircle, Edit3, Loader2 } from "lucide-react";
+import { AlertCircle, Edit3, Loader2, X } from "lucide-react";
import { useState } from "react";
import type { UserRecord, UserRole } from "@/lib/api";
import { manageableRoles } from "@/app/users/components/user-role-config";
@@ -27,16 +27,28 @@ export default function EditRoleModal({
return (
event.stopPropagation()}
+ >
+
+
+
+
@@ -53,19 +65,6 @@ export default function EditRoleModal({
{user.email}
-
- x
-
{error ? (
@@ -152,7 +151,8 @@ export default function EditRoleModal({
Save Role
+
);
-}
\ No newline at end of file
+}
diff --git a/src/app/users/components/InviteUserModal.tsx b/src/app/users/components/InviteUserModal.tsx
index 9f2b6eb..2585cb9 100644
--- a/src/app/users/components/InviteUserModal.tsx
+++ b/src/app/users/components/InviteUserModal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { AlertCircle, CheckCircle, Copy, Key, Loader2 } from "lucide-react";
+import { AlertCircle, CheckCircle, Copy, Key, Loader2, X } from "lucide-react";
import { useState } from "react";
import type {
CreateUserInvitationBody,
@@ -58,16 +58,28 @@ export default function InviteUserModal({
return (
event.stopPropagation()}
+ >
+
+
+
+
@@ -86,19 +98,6 @@ export default function InviteUserModal({
Generate an onboarding link so the user sets their own password.
-
- x
-
{result ? (
@@ -379,6 +378,7 @@ export default function InviteUserModal({
)}
+
);
diff --git a/src/app/users/components/ServerAccessModal.tsx b/src/app/users/components/ServerAccessModal.tsx
index 1f290f3..f2ba815 100644
--- a/src/app/users/components/ServerAccessModal.tsx
+++ b/src/app/users/components/ServerAccessModal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { AlertCircle, Loader2, Shield } from "lucide-react";
+import { AlertCircle, Loader2, Shield, X } from "lucide-react";
import { useState } from "react";
import type { Server as ServerRecord, UserRecord } from "@/lib/api";
import ServerAccessSelector from "@/app/users/components/ServerAccessSelector";
@@ -35,16 +35,28 @@ export default function ServerAccessModal({
return (
event.stopPropagation()}
+ >
+
+
+
+
@@ -61,19 +73,6 @@ export default function ServerAccessModal({
{user.email}
-
- x
-
{error ? (
@@ -140,7 +139,8 @@ export default function ServerAccessModal({
Save Access
+
);
-}
\ No newline at end of file
+}
diff --git a/src/components/ConfirmActionDialog.tsx b/src/components/ConfirmActionDialog.tsx
index 73c4440..214f8fe 100644
--- a/src/components/ConfirmActionDialog.tsx
+++ b/src/components/ConfirmActionDialog.tsx
@@ -73,21 +73,35 @@ export default function ConfirmActionDialog({
onClick={onClose}
>
event.stopPropagation()}
>
+
+
+
+
@@ -121,19 +135,6 @@ export default function ConfirmActionDialog({
{description}
-
-
-
{note ? (
@@ -176,6 +177,7 @@ export default function ConfirmActionDialog({
+
);