feat: Complete light mode implementation with semantic theme system (#450)

- Add comprehensive light/dark mode CSS variables with semantic naming
- Implement theme-aware scrollbars using CSS variables
- Add light mode backgrounds: --bg-base, --bg-elevated, --bg-surface, etc.
- Add theme-aware borders: --border-base, --border-panel, --border-subtle
- Add semantic text colors: --foreground-secondary, --foreground-subtle
- Convert oklch colors to hex for better compatibility
- Add theme awareness to CodeMirror editors
- Update dark mode colors for consistency (background, sidebar, card, muted, input)
- Add Tailwind color mappings for semantic classes

Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
This commit was merged in pull request #450.
This commit is contained in:
Peet McKinney
2025-12-23 15:35:49 -07:00
committed by GitHub
parent 186ba34c66
commit e6a70e3a02
84 changed files with 1084 additions and 664 deletions

View File

@@ -98,7 +98,7 @@ export function HostManager({
<div>
<div className="w-full">
<div
className="bg-dark-bg text-white p-4 pt-0 rounded-lg border-2 border-dark-border flex flex-col min-h-0 overflow-hidden"
className="bg-canvas text-foreground p-4 pt-0 rounded-lg border-2 border-edge flex flex-col min-h-0 overflow-hidden"
style={{
marginLeft: leftMarginPx,
marginRight: rightSidebarOpen
@@ -116,22 +116,22 @@ export function HostManager({
onValueChange={handleTabChange}
className="flex-1 flex flex-col h-full min-h-0"
>
<TabsList className="bg-dark-bg border-2 border-dark-border mt-1.5">
<TabsTrigger value="host_viewer">
<TabsList className="bg-elevated border-2 border-edge mt-1.5">
<TabsTrigger value="host_viewer" className="bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge">
{t("hosts.hostViewer")}
</TabsTrigger>
<TabsTrigger value="add_host">
<TabsTrigger value="add_host" className="bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge">
{editingHost
? editingHost.id
? t("hosts.editHost")
: t("hosts.cloneHost")
: t("hosts.addHost")}
</TabsTrigger>
<div className="h-6 w-px bg-dark-border mx-1"></div>
<TabsTrigger value="credentials">
<div className="h-6 w-px bg-border-base mx-1"></div>
<TabsTrigger value="credentials" className="bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge">
{t("credentials.credentialsViewer")}
</TabsTrigger>
<TabsTrigger value="add_credential">
<TabsTrigger value="add_credential" className="bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge">
{editingCredential
? t("credentials.editCredential")
: t("credentials.addCredential")}
@@ -161,7 +161,7 @@ export function HostManager({
className="flex-1 flex flex-col h-full min-h-0"
>
<Separator className="p-0.25 -mt-0.5 mb-1" />
<div className="flex flex-col h-full min-h-0 overflow-auto">
<div className="flex flex-col h-full min-h-0 overflow-auto thin-scrollbar">
<CredentialsManager onEditCredential={handleEditCredential} />
</div>
</TabsContent>

View File

@@ -140,7 +140,7 @@ function JumpHostItem({
<Command>
<CommandInput placeholder={t("hosts.searchServers")} />
<CommandEmpty>{t("hosts.noServerFound")}</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-y-auto">
<CommandGroup className="max-h-[300px] overflow-y-auto thin-scrollbar">
{hosts
.filter((h) => !editingHost || h.id !== editingHost.id)
.map((host) => (
@@ -246,7 +246,7 @@ function QuickActionItem({
<Command>
<CommandInput placeholder={t("hosts.searchSnippets")} />
<CommandEmpty>{t("hosts.noSnippetFound")}</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-y-auto">
<CommandGroup className="max-h-[300px] overflow-y-auto thin-scrollbar">
{snippets.map((snippet) => (
<CommandItem
key={snippet.id}
@@ -1159,19 +1159,19 @@ export function HostManagerEditor({
onValueChange={setActiveTab}
className="w-full"
>
<TabsList>
<TabsTrigger value="general">
<TabsList className="bg-button border border-edge-medium">
<TabsTrigger value="general" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">
{t("hosts.general")}
</TabsTrigger>
<TabsTrigger value="terminal">
<TabsTrigger value="terminal" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">
{t("hosts.terminal")}
</TabsTrigger>
<TabsTrigger value="docker">{t("hosts.docker")}</TabsTrigger>
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
<TabsTrigger value="file_manager">
<TabsTrigger value="docker" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">Docker</TabsTrigger>
<TabsTrigger value="tunnel" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">{t("hosts.tunnel")}</TabsTrigger>
<TabsTrigger value="file_manager" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">
{t("hosts.fileManager")}
</TabsTrigger>
<TabsTrigger value="statistics">
<TabsTrigger value="statistics" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">
{t("hosts.statistics")}
</TabsTrigger>
{!editingHost?.isShared && (
@@ -1307,7 +1307,7 @@ export function HostManagerEditor({
{folderDropdownOpen && filteredFolders.length > 0 && (
<div
ref={folderDropdownRef}
className="absolute top-full left-0 z-50 mt-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
className="absolute top-full left-0 z-50 mt-1 w-full bg-canvas border border-input rounded-md shadow-lg max-h-40 overflow-y-auto thin-scrollbar p-1"
>
<div className="grid grid-cols-1 gap-1 p-0">
{filteredFolders.map((folder) => (
@@ -1336,7 +1336,7 @@ export function HostManagerEditor({
<FormItem className="col-span-10 overflow-visible">
<FormLabel>{t("hosts.tags")}</FormLabel>
<FormControl>
<div className="flex flex-wrap items-center gap-1 border border-input rounded-md px-3 py-2 bg-dark-bg-input focus-within:ring-2 ring-ring min-h-[40px]">
<div className="flex flex-wrap items-center gap-1 border border-input rounded-md px-3 py-2 bg-field focus-within:ring-2 ring-ring min-h-[40px]">
{field.value.map((tag: string, idx: number) => (
<span
key={tag + idx}
@@ -1443,15 +1443,15 @@ export function HostManagerEditor({
}}
className="flex-1 flex flex-col h-full min-h-0"
>
<TabsList>
<TabsTrigger value="password">
<TabsList className="bg-button border border-edge-medium">
<TabsTrigger value="password" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">
{t("hosts.password")}
</TabsTrigger>
<TabsTrigger value="key">{t("hosts.key")}</TabsTrigger>
<TabsTrigger value="credential">
<TabsTrigger value="key" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">{t("hosts.key")}</TabsTrigger>
<TabsTrigger value="credential" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">
{t("hosts.credential")}
</TabsTrigger>
<TabsTrigger value="none">{t("hosts.none")}</TabsTrigger>
<TabsTrigger value="none" className="bg-button data-[state=active]:bg-elevated data-[state=active]:border data-[state=active]:border-edge-medium">{t("hosts.none")}</TabsTrigger>
</TabsList>
<TabsContent value="password">
<FormField
@@ -1573,6 +1573,8 @@ export function HostManagerEditor({
EditorView.theme({
".cm-scroller": {
overflow: "auto",
scrollbarWidth: "thin",
scrollbarColor: "var(--scrollbar-thumb) var(--scrollbar-track)",
},
}),
]}
@@ -1611,7 +1613,7 @@ export function HostManagerEditor({
ref={keyTypeButtonRef}
type="button"
variant="outline"
className="w-full justify-start text-left rounded-md px-2 py-2 bg-dark-bg border border-input text-foreground"
className="w-full justify-start text-left rounded-md px-2 py-2 bg-canvas border border-input text-foreground"
onClick={() =>
setKeyTypeDropdownOpen((open) => !open)
}
@@ -1623,7 +1625,7 @@ export function HostManagerEditor({
{keyTypeDropdownOpen && (
<div
ref={keyTypeDropdownRef}
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
className="absolute bottom-full left-0 z-50 mb-1 w-full bg-canvas border border-input rounded-md shadow-lg max-h-40 overflow-y-auto thin-scrollbar p-1"
>
<div className="grid grid-cols-1 gap-1 p-0">
{keyTypeOptions.map((opt) => (
@@ -1632,7 +1634,7 @@ export function HostManagerEditor({
type="button"
variant="ghost"
size="sm"
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-dark-bg text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
className="w-full justify-start text-left rounded-md px-2 py-1.5 bg-canvas text-foreground hover:bg-white/15 focus:bg-white/20 focus:outline-none"
onClick={() => {
field.onChange(opt.value);
setKeyTypeDropdownOpen(false);
@@ -1686,7 +1688,7 @@ export function HostManagerEditor({
control={form.control}
name="overrideCredentialUsername"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.overrideCredentialUsername")}
@@ -1732,7 +1734,7 @@ export function HostManagerEditor({
control={form.control}
name="forceKeyboardInteractive"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.forceKeyboardInteractive")}
@@ -2353,7 +2355,7 @@ export function HostManagerEditor({
control={form.control}
name="terminalConfig.cursorBlink"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.cursorBlink")}</FormLabel>
<FormDescription>
@@ -2446,7 +2448,7 @@ export function HostManagerEditor({
control={form.control}
name="terminalConfig.rightClickSelectsWord"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.rightClickSelectsWord")}
@@ -2568,7 +2570,7 @@ export function HostManagerEditor({
control={form.control}
name="terminalConfig.agentForwarding"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.sshAgentForwarding")}
@@ -2666,7 +2668,7 @@ export function HostManagerEditor({
<CommandEmpty>
{t("hosts.noSnippetFound")}
</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-y-auto">
<CommandGroup className="max-h-[300px] overflow-y-auto thin-scrollbar">
<CommandItem
value="none"
onSelect={() => {
@@ -2727,7 +2729,7 @@ export function HostManagerEditor({
control={form.control}
name="terminalConfig.autoMosh"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.autoMosh")}</FormLabel>
<FormDescription>
@@ -3152,7 +3154,7 @@ export function HostManagerEditor({
index
] = el;
}}
className="absolute top-full left-0 z-50 mt-1 w-full bg-dark-bg border border-input rounded-md shadow-lg max-h-40 overflow-y-auto p-1"
className="absolute top-full left-0 z-50 mt-1 w-full bg-canvas border border-input rounded-md shadow-lg max-h-40 overflow-y-auto thin-scrollbar p-1"
>
<div className="grid grid-cols-1 gap-1 p-0">
{getFilteredSshConfigs(
@@ -3364,7 +3366,7 @@ export function HostManagerEditor({
control={form.control}
name="statsConfig.statusCheckEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>
{t("hosts.statusCheckEnabled")}
@@ -3461,7 +3463,7 @@ export function HostManagerEditor({
control={form.control}
name="statsConfig.metricsEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 bg-elevated dark:bg-input/30">
<div className="space-y-0.5">
<FormLabel>{t("hosts.metricsEnabled")}</FormLabel>
<FormDescription>

View File

@@ -1155,7 +1155,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
draggable
onDragStart={(e) => handleDragStart(e, host)}
onDragEnd={handleDragEnd}
className={`bg-dark-bg-input border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-dark-hover-alt transition-all duration-200 p-3 group relative ${
className={`bg-field border border-input rounded-lg cursor-pointer hover:shadow-lg hover:border-blue-400/50 hover:bg-hover-alt transition-all duration-200 p-3 group relative ${
draggedHost?.id === host.id
? "opacity-50 scale-95"
: ""

View File

@@ -343,7 +343,7 @@ export function HostSharingTab({
<Command>
<CommandInput placeholder={t("rbac.searchUsers")} />
<CommandEmpty>{t("rbac.noUserFound")}</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-y-auto">
<CommandGroup className="max-h-[300px] overflow-y-auto thin-scrollbar">
{availableUsers.map((user) => (
<CommandItem
key={user.id}
@@ -396,7 +396,7 @@ export function HostSharingTab({
<Command>
<CommandInput placeholder={t("rbac.searchRoles")} />
<CommandEmpty>{t("rbac.noRoleFound")}</CommandEmpty>
<CommandGroup className="max-h-[300px] overflow-y-auto">
<CommandGroup className="max-h-[300px] overflow-y-auto thin-scrollbar">
{roles.map((role) => (
<CommandItem
key={role.id}

View File

@@ -94,7 +94,7 @@ export function FolderEditDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px] bg-dark-bg border-2 border-dark-border">
<DialogContent className="sm:max-w-[500px] bg-canvas border-2 border-edge">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Folder className="w-5 h-5" />
@@ -119,7 +119,7 @@ export function FolderEditDialog({
className={`h-12 rounded-md border-2 transition-all hover:scale-105 ${
selectedColor === color.value
? "border-white shadow-lg scale-105"
: "border-dark-border"
: "border-edge"
}`}
style={{ backgroundColor: color.value }}
onClick={() => setSelectedColor(color.value)}
@@ -141,7 +141,7 @@ export function FolderEditDialog({
className={`h-14 rounded-md border-2 transition-all hover:scale-105 flex items-center justify-center ${
selectedIcon === value
? "border-primary bg-primary/10"
: "border-dark-border bg-dark-bg-darker"
: "border-edge bg-elevated"
}`}
onClick={() => setSelectedIcon(value)}
title={label}
@@ -156,7 +156,7 @@ export function FolderEditDialog({
<Label className="text-base font-semibold text-foreground">
{t("hosts.preview")}
</Label>
<div className="flex items-center gap-3 p-4 rounded-md bg-dark-bg-darker border border-dark-border">
<div className="flex items-center gap-3 p-4 rounded-md bg-elevated border border-edge">
{(() => {
const IconComponent =
AVAILABLE_ICONS.find((i) => i.value === selectedIcon)?.Icon ||