refactor: move configs and hooks to global locations for reusability

Move previously nested configs and hooks to global src/ folders to make
them reusable across the application, reduce nesting, and establish
clearer organization patterns.

**New Global Structure:**
- src/config/theme-options.ts (moved from appearance/config/)
- src/config/api-providers.ts (moved from api-keys/config/)
- src/hooks/use-scroll-tracking.ts (moved from settings-view/hooks/)

**Changes:**
- Move theme-options.ts to src/config/ - app-wide theme configuration
- Move api-provider-config.ts to src/config/api-providers.ts - global API config
- Move use-scroll-tracking.ts to src/hooks/ - reusable scroll navigation hook
- Make useScrollTracking generic and more flexible with options object
- Update all imports across settings-view components
- Remove duplicate api-provider-config.ts from shared/ folder
- Remove empty config/ folders (appearance/config, api-keys/config)

**Benefits:**
 Single source of truth for themes and API providers
 Reusable scroll tracking hook available globally
 Cleaner structure with less nesting
 Better discoverability for developers
 No duplicate configuration files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Kacper
2025-12-11 01:10:36 +01:00
parent 82cc8abd29
commit 772b0e9e5c
9 changed files with 56 additions and 180 deletions

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { useAppStore } from "@/store/app-store";
import { Button } from "@/components/ui/button";
import { useCliStatus } from "./settings-view/hooks/use-cli-status";
import { useScrollTracking } from "./settings-view/hooks/use-scroll-tracking";
import { useScrollTracking } from "@/hooks/use-scroll-tracking";
import { NAV_ITEMS } from "./settings-view/config/navigation";
import { SettingsHeader } from "./settings-view/components/settings-header";
import { KeyboardMapDialog } from "./settings-view/components/keyboard-map-dialog";
@@ -76,7 +76,11 @@ export function SettingsView() {
// Use scroll tracking hook
const { activeSection, scrollToSection, scrollContainerRef } =
useScrollTracking(NAV_ITEMS, currentProject);
useScrollTracking({
items: NAV_ITEMS,
filterFn: (item) => item.id !== "danger" || !!currentProject,
initialSection: "api-keys",
});
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);

View File

@@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { AlertCircle, CheckCircle2, Eye, EyeOff, Loader2, Zap } from "lucide-react";
import type { ProviderConfig } from "./shared/api-provider-config";
import type { ProviderConfig } from "@/config/api-providers";
interface ApiKeyFieldProps {
config: ProviderConfig;

View File

@@ -3,7 +3,7 @@ import { useSetupStore } from "@/store/setup-store";
import { Button } from "@/components/ui/button";
import { Key, CheckCircle2 } from "lucide-react";
import { ApiKeyField } from "./api-key-field";
import { buildProviderConfigs } from "./shared/api-provider-config";
import { buildProviderConfigs } from "@/config/api-providers";
import { AuthenticationStatusDisplay } from "./authentication-status-display";
import { SecurityNotice } from "./security-notice";
import { useApiKeyManagement } from "./hooks/use-api-key-management";

View File

@@ -1,149 +0,0 @@
import type { Dispatch, SetStateAction } from "react";
import type { LucideIcon } from "lucide-react";
import type { ApiKeys } from "@/store/app-store";
export type ProviderKey = "anthropic" | "google" | "openai";
export interface ProviderConfig {
key: ProviderKey;
label: string;
inputId: string;
placeholder: string;
value: string;
setValue: Dispatch<SetStateAction<string>>;
showValue: boolean;
setShowValue: Dispatch<SetStateAction<boolean>>;
hasStoredKey: string | null | undefined;
inputTestId: string;
toggleTestId: string;
testButton: {
onClick: () => Promise<void> | void;
disabled: boolean;
loading: boolean;
testId: string;
};
result: { success: boolean; message: string } | null;
resultTestId: string;
resultMessageTestId: string;
descriptionPrefix: string;
descriptionLinkHref: string;
descriptionLinkText: string;
descriptionSuffix?: string;
}
export interface ProviderConfigParams {
apiKeys: ApiKeys;
anthropic: {
value: string;
setValue: Dispatch<SetStateAction<string>>;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
testing: boolean;
onTest: () => Promise<void>;
result: { success: boolean; message: string } | null;
};
google: {
value: string;
setValue: Dispatch<SetStateAction<string>>;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
testing: boolean;
onTest: () => Promise<void>;
result: { success: boolean; message: string } | null;
};
openai: {
value: string;
setValue: Dispatch<SetStateAction<string>>;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
testing: boolean;
onTest: () => Promise<void>;
result: { success: boolean; message: string } | null;
};
}
export const buildProviderConfigs = ({
apiKeys,
anthropic,
google,
openai,
}: ProviderConfigParams): ProviderConfig[] => [
{
key: "anthropic",
label: "Anthropic API Key (Claude)",
inputId: "anthropic-key",
placeholder: "sk-ant-...",
value: anthropic.value,
setValue: anthropic.setValue,
showValue: anthropic.show,
setShowValue: anthropic.setShow,
hasStoredKey: apiKeys.anthropic,
inputTestId: "anthropic-api-key-input",
toggleTestId: "toggle-anthropic-visibility",
testButton: {
onClick: anthropic.onTest,
disabled: !anthropic.value || anthropic.testing,
loading: anthropic.testing,
testId: "test-claude-connection",
},
result: anthropic.result,
resultTestId: "test-connection-result",
resultMessageTestId: "test-connection-message",
descriptionPrefix: "Used for Claude AI features. Get your key at",
descriptionLinkHref: "https://console.anthropic.com/account/keys",
descriptionLinkText: "console.anthropic.com",
descriptionSuffix:
". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.",
},
{
key: "google",
label: "Google API Key (Gemini)",
inputId: "google-key",
placeholder: "AIza...",
value: google.value,
setValue: google.setValue,
showValue: google.show,
setShowValue: google.setShow,
hasStoredKey: apiKeys.google,
inputTestId: "google-api-key-input",
toggleTestId: "toggle-google-visibility",
testButton: {
onClick: google.onTest,
disabled: !google.value || google.testing,
loading: google.testing,
testId: "test-gemini-connection",
},
result: google.result,
resultTestId: "gemini-test-connection-result",
resultMessageTestId: "gemini-test-connection-message",
descriptionPrefix:
"Used for Gemini AI features (including image/design prompts). Get your key at",
descriptionLinkHref: "https://makersuite.google.com/app/apikey",
descriptionLinkText: "makersuite.google.com",
},
{
key: "openai",
label: "OpenAI API Key (Codex/GPT)",
inputId: "openai-key",
placeholder: "sk-...",
value: openai.value,
setValue: openai.setValue,
showValue: openai.show,
setShowValue: openai.setShow,
hasStoredKey: apiKeys.openai,
inputTestId: "openai-api-key-input",
toggleTestId: "toggle-openai-visibility",
testButton: {
onClick: openai.onTest,
disabled: !openai.value || openai.testing,
loading: openai.testing,
testId: "test-openai-connection",
},
result: openai.result,
resultTestId: "openai-test-connection-result",
resultMessageTestId: "openai-test-connection-message",
descriptionPrefix: "Used for OpenAI Codex CLI and GPT models. Get your key at",
descriptionLinkHref: "https://platform.openai.com/api-keys",
descriptionLinkText: "platform.openai.com",
},
];

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import type { ProviderConfigParams } from "../config/api-provider-config";
import type { ProviderConfigParams } from "@/config/api-providers";
interface TestResult {
success: boolean;

View File

@@ -1,7 +1,7 @@
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Palette } from "lucide-react";
import { themeOptions } from "./config/theme-options";
import { themeOptions } from "@/config/theme-options";
import type { Theme, Project } from "../shared/types";
interface AppearanceSectionProps {

View File

@@ -1,88 +0,0 @@
import {
type LucideIcon,
Atom,
Cat,
Eclipse,
Flame,
Ghost,
Moon,
Radio,
Snowflake,
Sparkles,
Sun,
Terminal,
Trees,
} from "lucide-react";
import { Theme } from "../../shared/types";
export interface ThemeOption {
value: Theme;
label: string;
Icon: LucideIcon;
testId: string;
}
export const themeOptions: ReadonlyArray<ThemeOption> = [
{ value: "dark", label: "Dark", Icon: Moon, testId: "dark-mode-button" },
{ value: "light", label: "Light", Icon: Sun, testId: "light-mode-button" },
{
value: "retro",
label: "Retro",
Icon: Terminal,
testId: "retro-mode-button",
},
{
value: "dracula",
label: "Dracula",
Icon: Ghost,
testId: "dracula-mode-button",
},
{
value: "nord",
label: "Nord",
Icon: Snowflake,
testId: "nord-mode-button",
},
{
value: "monokai",
label: "Monokai",
Icon: Flame,
testId: "monokai-mode-button",
},
{
value: "tokyonight",
label: "Tokyo Night",
Icon: Sparkles,
testId: "tokyonight-mode-button",
},
{
value: "solarized",
label: "Solarized",
Icon: Eclipse,
testId: "solarized-mode-button",
},
{
value: "gruvbox",
label: "Gruvbox",
Icon: Trees,
testId: "gruvbox-mode-button",
},
{
value: "catppuccin",
label: "Catppuccin",
Icon: Cat,
testId: "catppuccin-mode-button",
},
{
value: "onedark",
label: "One Dark",
Icon: Atom,
testId: "onedark-mode-button",
},
{
value: "synthwave",
label: "Synthwave",
Icon: Radio,
testId: "synthwave-mode-button",
},
];

View File

@@ -1,84 +0,0 @@
import { useState, useEffect, useRef, useCallback } from "react";
import type { Project } from "@/store/app-store";
import type { NavigationItem } from "../config/navigation";
/**
* Custom hook for managing scroll-based navigation tracking
* Automatically highlights the active section based on scroll position
* and provides smooth scrolling to sections
*/
export function useScrollTracking(
navItems: NavigationItem[],
currentProject: Project | null
) {
const [activeSection, setActiveSection] = useState("api-keys");
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Track scroll position to highlight active nav item
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
const handleScroll = () => {
const sections = navItems
.filter((item) => item.id !== "danger" || currentProject)
.map((item) => ({
id: item.id,
element: document.getElementById(item.id),
}))
.filter((s) => s.element);
const containerRect = container.getBoundingClientRect();
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
// Check if scrolled to bottom (within a small threshold)
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 50;
if (isAtBottom && sections.length > 0) {
// If at bottom, highlight the last visible section
setActiveSection(sections[sections.length - 1].id);
return;
}
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
if (section.element) {
const rect = section.element.getBoundingClientRect();
const relativeTop = rect.top - containerRect.top + scrollTop;
if (scrollTop >= relativeTop - 100) {
setActiveSection(section.id);
break;
}
}
}
};
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, [currentProject, navItems]);
// Scroll to a specific section with smooth animation
const scrollToSection = useCallback((sectionId: string) => {
const element = document.getElementById(sectionId);
if (element && scrollContainerRef.current) {
const container = scrollContainerRef.current;
const containerRect = container.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const relativeTop =
elementRect.top - containerRect.top + container.scrollTop;
container.scrollTo({
top: relativeTop - 24,
behavior: "smooth",
});
}
}, []);
return {
activeSection,
scrollToSection,
scrollContainerRef,
};
}

View File

@@ -1,148 +0,0 @@
import type { Dispatch, SetStateAction } from "react";
import type { ApiKeys } from "@/store/app-store";
export type ProviderKey = "anthropic" | "google" | "openai";
export interface ProviderConfig {
key: ProviderKey;
label: string;
inputId: string;
placeholder: string;
value: string;
setValue: Dispatch<SetStateAction<string>>;
showValue: boolean;
setShowValue: Dispatch<SetStateAction<boolean>>;
hasStoredKey: string | null | undefined;
inputTestId: string;
toggleTestId: string;
testButton: {
onClick: () => Promise<void> | void;
disabled: boolean;
loading: boolean;
testId: string;
};
result: { success: boolean; message: string } | null;
resultTestId: string;
resultMessageTestId: string;
descriptionPrefix: string;
descriptionLinkHref: string;
descriptionLinkText: string;
descriptionSuffix?: string;
}
export interface ProviderConfigParams {
apiKeys: ApiKeys;
anthropic: {
value: string;
setValue: Dispatch<SetStateAction<string>>;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
testing: boolean;
onTest: () => Promise<void>;
result: { success: boolean; message: string } | null;
};
google: {
value: string;
setValue: Dispatch<SetStateAction<string>>;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
testing: boolean;
onTest: () => Promise<void>;
result: { success: boolean; message: string } | null;
};
openai: {
value: string;
setValue: Dispatch<SetStateAction<string>>;
show: boolean;
setShow: Dispatch<SetStateAction<boolean>>;
testing: boolean;
onTest: () => Promise<void>;
result: { success: boolean; message: string } | null;
};
}
export const buildProviderConfigs = ({
apiKeys,
anthropic,
google,
openai,
}: ProviderConfigParams): ProviderConfig[] => [
{
key: "anthropic",
label: "Anthropic API Key (Claude)",
inputId: "anthropic-key",
placeholder: "sk-ant-...",
value: anthropic.value,
setValue: anthropic.setValue,
showValue: anthropic.show,
setShowValue: anthropic.setShow,
hasStoredKey: apiKeys.anthropic,
inputTestId: "anthropic-api-key-input",
toggleTestId: "toggle-anthropic-visibility",
testButton: {
onClick: anthropic.onTest,
disabled: !anthropic.value || anthropic.testing,
loading: anthropic.testing,
testId: "test-claude-connection",
},
result: anthropic.result,
resultTestId: "test-connection-result",
resultMessageTestId: "test-connection-message",
descriptionPrefix: "Used for Claude AI features. Get your key at",
descriptionLinkHref: "https://console.anthropic.com/account/keys",
descriptionLinkText: "console.anthropic.com",
descriptionSuffix:
". Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.",
},
{
key: "google",
label: "Google API Key (Gemini)",
inputId: "google-key",
placeholder: "AIza...",
value: google.value,
setValue: google.setValue,
showValue: google.show,
setShowValue: google.setShow,
hasStoredKey: apiKeys.google,
inputTestId: "google-api-key-input",
toggleTestId: "toggle-google-visibility",
testButton: {
onClick: google.onTest,
disabled: !google.value || google.testing,
loading: google.testing,
testId: "test-gemini-connection",
},
result: google.result,
resultTestId: "gemini-test-connection-result",
resultMessageTestId: "gemini-test-connection-message",
descriptionPrefix:
"Used for Gemini AI features (including image/design prompts). Get your key at",
descriptionLinkHref: "https://makersuite.google.com/app/apikey",
descriptionLinkText: "makersuite.google.com",
},
{
key: "openai",
label: "OpenAI API Key (Codex/GPT)",
inputId: "openai-key",
placeholder: "sk-...",
value: openai.value,
setValue: openai.setValue,
showValue: openai.show,
setShowValue: openai.setShow,
hasStoredKey: apiKeys.openai,
inputTestId: "openai-api-key-input",
toggleTestId: "toggle-openai-visibility",
testButton: {
onClick: openai.onTest,
disabled: !openai.value || openai.testing,
loading: openai.testing,
testId: "test-openai-connection",
},
result: openai.result,
resultTestId: "openai-test-connection-result",
resultMessageTestId: "openai-test-connection-message",
descriptionPrefix: "Used for OpenAI Codex CLI and GPT models. Get your key at",
descriptionLinkHref: "https://platform.openai.com/api-keys",
descriptionLinkText: "platform.openai.com",
},
];