feat(language): implement internationalization and help overlay
This update introduces comprehensive language support (English/German) across the application, enabling localization for UI elements and settings dialogs. It also adds a dedicated Help Overlay component with detailed guides on using Gitty's core Git features. - Adds language selection to App Settings - Implements German translations in key areas - Introduces global help documentation accessible via Ctrl+/
This commit is contained in:
+55
-3
@@ -21,6 +21,7 @@
|
|||||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||||
|
import HelpOverlay from "./lib/components/HelpOverlay.svelte";
|
||||||
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
import HistoryPanel from "./lib/components/HistoryPanel.svelte";
|
||||||
import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte";
|
import InteractiveRebaseDialog from "./lib/components/InteractiveRebaseDialog.svelte";
|
||||||
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
|
import LinePatchDialog from "./lib/components/LinePatchDialog.svelte";
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AiSettings,
|
AiSettings,
|
||||||
|
AppLanguage,
|
||||||
AppTheme,
|
AppTheme,
|
||||||
AnalyticsSettings,
|
AnalyticsSettings,
|
||||||
CommitAiPhase,
|
CommitAiPhase,
|
||||||
@@ -176,6 +178,7 @@
|
|||||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||||
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
||||||
const APP_THEME_KEY = "gitlite.theme.v1";
|
const APP_THEME_KEY = "gitlite.theme.v1";
|
||||||
|
const APP_LANGUAGE_KEY = "gitlite.language.v1";
|
||||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||||
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
const LEFT_SIDEBAR_WIDTH_KEY = "gitlite.leftSidebarWidth.v1";
|
||||||
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
const LEFT_BRANCH_PANEL_HEIGHT_KEY = "gitlite.leftBranchPanelHeight.v1";
|
||||||
@@ -248,9 +251,11 @@
|
|||||||
let aiSettings: AiSettings = defaultAiSettings();
|
let aiSettings: AiSettings = defaultAiSettings();
|
||||||
let aiSettingsOpen = false;
|
let aiSettingsOpen = false;
|
||||||
let appSettingsOpen = false;
|
let appSettingsOpen = false;
|
||||||
|
let helpOpen = false;
|
||||||
let analyticsNoticeOpen = false;
|
let analyticsNoticeOpen = false;
|
||||||
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
||||||
let appTheme: AppTheme = loadThemePreference();
|
let appTheme: AppTheme = loadThemePreference();
|
||||||
|
let appLanguage: AppLanguage = loadLanguagePreference();
|
||||||
let localModelOptions: LocalModelOption[] = [];
|
let localModelOptions: LocalModelOption[] = [];
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
let operation = "";
|
let operation = "";
|
||||||
@@ -412,6 +417,7 @@
|
|||||||
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
$: allLeftPanelsCollapsed = branchPanelCollapsed && stashPanelCollapsed && explorerPanelCollapsed;
|
||||||
|
|
||||||
$: applyThemePreference(appTheme);
|
$: applyThemePreference(appTheme);
|
||||||
|
$: applyLanguagePreference(appLanguage);
|
||||||
|
|
||||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -672,7 +678,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function autoRefreshTick() {
|
async function autoRefreshTick() {
|
||||||
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen) return;
|
if (!autoRefreshEnabled || activeView !== "repository" || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || compareSelectOpen || interactiveRebaseOpen || reflogOpen || newBranchCommit || globalSearchOpen || helpOpen) return;
|
||||||
const path = activeRepoPath;
|
const path = activeRepoPath;
|
||||||
autoRefreshInFlight = true;
|
autoRefreshInFlight = true;
|
||||||
try {
|
try {
|
||||||
@@ -813,6 +819,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadLanguagePreference(): AppLanguage {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(APP_LANGUAGE_KEY);
|
||||||
|
if (stored === "en" || stored === "de") return stored;
|
||||||
|
} catch {
|
||||||
|
// Local storage is optional; English is the first-start default.
|
||||||
|
}
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistLanguagePreference(next: AppLanguage) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(APP_LANGUAGE_KEY, next);
|
||||||
|
} catch {
|
||||||
|
// Ignore storage quota/private-mode errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLanguagePreference(next: AppLanguage) {
|
||||||
|
document.documentElement.lang = next;
|
||||||
|
document.documentElement.dataset.language = next;
|
||||||
|
}
|
||||||
|
|
||||||
function applyThemePreference(next: AppTheme) {
|
function applyThemePreference(next: AppTheme) {
|
||||||
const prefersLight = themeMediaQuery?.matches ?? window.matchMedia("(prefers-color-scheme: light)").matches;
|
const prefersLight = themeMediaQuery?.matches ?? window.matchMedia("(prefers-color-scheme: light)").matches;
|
||||||
const resolved = next === "system"
|
const resolved = next === "system"
|
||||||
@@ -828,13 +857,15 @@
|
|||||||
if (appTheme === "system") applyThemePreference(appTheme);
|
if (appTheme === "system") applyThemePreference(appTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme) {
|
function saveAppSettings(next: AnalyticsSettings, nextTheme: AppTheme, nextLanguage: AppLanguage) {
|
||||||
analyticsSettings = next;
|
analyticsSettings = next;
|
||||||
appTheme = nextTheme;
|
appTheme = nextTheme;
|
||||||
|
appLanguage = nextLanguage;
|
||||||
persistAnalyticsSettings(next);
|
persistAnalyticsSettings(next);
|
||||||
persistThemePreference(nextTheme);
|
persistThemePreference(nextTheme);
|
||||||
|
persistLanguagePreference(nextLanguage);
|
||||||
appSettingsOpen = false;
|
appSettingsOpen = false;
|
||||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme });
|
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCommitMessage(message: string) {
|
function updateCommitMessage(message: string) {
|
||||||
@@ -3259,6 +3290,11 @@
|
|||||||
trackEvent("global_search_opened");
|
trackEvent("global_search_opened");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openHelp() {
|
||||||
|
helpOpen = true;
|
||||||
|
trackEvent("help_opened");
|
||||||
|
}
|
||||||
|
|
||||||
async function compareSelectedCommits() {
|
async function compareSelectedCommits() {
|
||||||
if (!canCompare) return;
|
if (!canCompare) return;
|
||||||
await runOperation("Comparing commits", async () => {
|
await runOperation("Comparing commits", async () => {
|
||||||
@@ -3449,6 +3485,15 @@
|
|||||||
// ── Event handlers ─────────────────────────────────────────────────────────
|
// ── Event handlers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function handleWindowKeydown(event: KeyboardEvent) {
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.key === "/") {
|
||||||
|
event.preventDefault();
|
||||||
|
openHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key === "Escape" && helpOpen) {
|
||||||
|
helpOpen = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === "Escape" && repoTabContextMenu) closeRepoTabContextMenu();
|
if (event.key === "Escape" && repoTabContextMenu) closeRepoTabContextMenu();
|
||||||
else if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
|
else if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
|
||||||
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||||
@@ -3499,6 +3544,8 @@
|
|||||||
onOpenInExplorer={openActiveRepoInExplorer}
|
onOpenInExplorer={openActiveRepoInExplorer}
|
||||||
onToggleAutoRefresh={toggleAutoRefresh}
|
onToggleAutoRefresh={toggleAutoRefresh}
|
||||||
onOpenSettings={() => { appSettingsOpen = true; }}
|
onOpenSettings={() => { appSettingsOpen = true; }}
|
||||||
|
onOpenHelp={openHelp}
|
||||||
|
language={appLanguage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="shell-body">
|
<div class="shell-body">
|
||||||
@@ -4113,11 +4160,16 @@
|
|||||||
<AppSettingsDialog
|
<AppSettingsDialog
|
||||||
analytics={analyticsSettings}
|
analytics={analyticsSettings}
|
||||||
theme={appTheme}
|
theme={appTheme}
|
||||||
|
language={appLanguage}
|
||||||
onSave={saveAppSettings}
|
onSave={saveAppSettings}
|
||||||
onClose={() => { appSettingsOpen = false; }}
|
onClose={() => { appSettingsOpen = false; }}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if helpOpen}
|
||||||
|
<HelpOverlay language={appLanguage} onClose={() => { helpOpen = false; }} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if linePatchOpen && linePatchFile}
|
{#if linePatchOpen && linePatchFile}
|
||||||
<LinePatchDialog
|
<LinePatchDialog
|
||||||
file={linePatchFile}
|
file={linePatchFile}
|
||||||
|
|||||||
@@ -3083,6 +3083,10 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-segmented.settings-language {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.settings-toggle-row {
|
.settings-toggle-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
|||||||
+16
-4
@@ -2,7 +2,7 @@
|
|||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { getVersion } from "@tauri-apps/api/app";
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, History, ListRestart, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
|
import { CircleHelp, CloudDownload, Download, FolderOpen, GitBranch, GitCompare, History, ListRestart, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
|
||||||
import iconUrl from "../../src-tauri/icons/icon.png";
|
import iconUrl from "../../src-tauri/icons/icon.png";
|
||||||
|
|
||||||
export let branch: string = "";
|
export let branch: string = "";
|
||||||
@@ -24,7 +24,9 @@
|
|||||||
export let onReflog: () => void = () => {};
|
export let onReflog: () => void = () => {};
|
||||||
export let onOpenInExplorer: () => void = () => {};
|
export let onOpenInExplorer: () => void = () => {};
|
||||||
export let onToggleAutoRefresh: () => void = () => {};
|
export let onToggleAutoRefresh: () => void = () => {};
|
||||||
|
export let onOpenHelp: () => void = () => {};
|
||||||
export let onOpenSettings: () => void = () => {};
|
export let onOpenSettings: () => void = () => {};
|
||||||
|
export let language: "en" | "de" = "en";
|
||||||
|
|
||||||
let win: ReturnType<typeof getCurrentWindow> | null = null;
|
let win: ReturnType<typeof getCurrentWindow> | null = null;
|
||||||
let isMaximized = false;
|
let isMaximized = false;
|
||||||
@@ -238,14 +240,24 @@
|
|||||||
<span class="tb-action-label">Auto</span>
|
<span class="tb-action-label">Auto</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="tb-action"
|
||||||
|
onclick={onOpenHelp}
|
||||||
|
title={language === "de" ? "Hilfe (Ctrl+/)" : "Help (Ctrl+/)"}
|
||||||
|
aria-label={language === "de" ? "Hilfe öffnen" : "Open help"}
|
||||||
|
>
|
||||||
|
<CircleHelp size={14} aria-hidden="true" />
|
||||||
|
<span class="tb-action-label">{language === "de" ? "Hilfe" : "Help"}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="tb-action"
|
class="tb-action"
|
||||||
onclick={onOpenSettings}
|
onclick={onOpenSettings}
|
||||||
title="Settings"
|
title={language === "de" ? "Einstellungen" : "Settings"}
|
||||||
aria-label="Settings"
|
aria-label={language === "de" ? "Einstellungen" : "Settings"}
|
||||||
>
|
>
|
||||||
<Settings size={14} aria-hidden="true" />
|
<Settings size={14} aria-hidden="true" />
|
||||||
<span class="tb-action-label">Settings</span>
|
<span class="tb-action-label">{language === "de" ? "Einstellungen" : "Settings"}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Check, Settings, X } from "@lucide/svelte";
|
import { Check, Languages, Settings, X } from "@lucide/svelte";
|
||||||
import type { AnalyticsSettings, AppTheme } from "../types";
|
import type { AnalyticsSettings, AppLanguage, AppTheme } from "../types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
analytics: AnalyticsSettings;
|
analytics: AnalyticsSettings;
|
||||||
theme: AppTheme;
|
theme: AppTheme;
|
||||||
onSave: (settings: AnalyticsSettings, theme: AppTheme) => void;
|
language: AppLanguage;
|
||||||
|
onSave: (settings: AnalyticsSettings, theme: AppTheme, language: AppLanguage) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { analytics, theme = "system", onSave = () => {}, onClose = () => {} }: Props = $props();
|
let { analytics, theme = "system", language = "en", onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||||
|
|
||||||
let analyticsEnabled = $state(true);
|
let analyticsEnabled = $state(true);
|
||||||
let selectedTheme = $state<AppTheme>("system");
|
let selectedTheme = $state<AppTheme>("system");
|
||||||
|
let selectedLanguage = $state<AppLanguage>("en");
|
||||||
|
const isGerman = $derived(selectedLanguage === "de");
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
analyticsEnabled = analytics.enabled;
|
analyticsEnabled = analytics.enabled;
|
||||||
selectedTheme = theme;
|
selectedTheme = theme;
|
||||||
|
selectedLanguage = language;
|
||||||
});
|
});
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
@@ -24,18 +28,18 @@
|
|||||||
...analytics,
|
...analytics,
|
||||||
enabled: analyticsEnabled,
|
enabled: analyticsEnabled,
|
||||||
noticeSeen: true,
|
noticeSeen: true,
|
||||||
}, selectedTheme);
|
}, selectedTheme, selectedLanguage);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dialog-backdrop" role="presentation">
|
<div class="dialog-backdrop" role="presentation">
|
||||||
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1">
|
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label={isGerman ? "Einstellungen" : "Settings"} tabindex="-1">
|
||||||
<header class="dialog-header">
|
<header class="dialog-header">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Gitty</span>
|
<span class="eyebrow">Gitty</span>
|
||||||
<h2 class="dialog-title">Settings</h2>
|
<h2 class="dialog-title">{isGerman ? "Einstellungen" : "Settings"}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
<button class="dialog-close" type="button" onclick={onClose} title={isGerman ? "Schließen" : "Close"}>
|
||||||
<X size={18} aria-hidden="true" />
|
<X size={18} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -45,23 +49,44 @@
|
|||||||
<header>
|
<header>
|
||||||
<Settings size={16} aria-hidden="true" />
|
<Settings size={16} aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Appearance</span>
|
<span class="eyebrow">{isGerman ? "Darstellung" : "Appearance"}</span>
|
||||||
<h3>Theme</h3>
|
<h3>{isGerman ? "Farbschema" : "Theme"}</h3>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="settings-segmented" role="radiogroup" aria-label="Theme">
|
<div class="settings-segmented" role="radiogroup" aria-label={isGerman ? "Farbschema" : "Theme"}>
|
||||||
<label class:active={selectedTheme === "system"}>
|
<label class:active={selectedTheme === "system"}>
|
||||||
<input type="radio" bind:group={selectedTheme} value="system" />
|
<input type="radio" bind:group={selectedTheme} value="system" />
|
||||||
<span>System</span>
|
<span>System</span>
|
||||||
</label>
|
</label>
|
||||||
<label class:active={selectedTheme === "light"}>
|
<label class:active={selectedTheme === "light"}>
|
||||||
<input type="radio" bind:group={selectedTheme} value="light" />
|
<input type="radio" bind:group={selectedTheme} value="light" />
|
||||||
<span>Light</span>
|
<span>{isGerman ? "Hell" : "Light"}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class:active={selectedTheme === "dark"}>
|
<label class:active={selectedTheme === "dark"}>
|
||||||
<input type="radio" bind:group={selectedTheme} value="dark" />
|
<input type="radio" bind:group={selectedTheme} value="dark" />
|
||||||
<span>Dark</span>
|
<span>{isGerman ? "Dunkel" : "Dark"}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<header>
|
||||||
|
<Languages size={16} aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">{isGerman ? "Sprache" : "Language"}</span>
|
||||||
|
<h3>{isGerman ? "App-Sprache" : "App language"}</h3>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="settings-segmented settings-language" role="radiogroup" aria-label={isGerman ? "App-Sprache" : "App language"}>
|
||||||
|
<label class:active={selectedLanguage === "en"}>
|
||||||
|
<input type="radio" bind:group={selectedLanguage} value="en" />
|
||||||
|
<span>EN · English</span>
|
||||||
|
</label>
|
||||||
|
<label class:active={selectedLanguage === "de"}>
|
||||||
|
<input type="radio" bind:group={selectedLanguage} value="de" />
|
||||||
|
<span>DE · Deutsch</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -71,24 +96,24 @@
|
|||||||
<Settings size={16} aria-hidden="true" />
|
<Settings size={16} aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Analytics</span>
|
<span class="eyebrow">Analytics</span>
|
||||||
<h3>Anonymous usage analytics</h3>
|
<h3>{isGerman ? "Anonyme Nutzungsanalyse" : "Anonymous usage analytics"}</h3>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<label class="settings-toggle-row">
|
<label class="settings-toggle-row">
|
||||||
<input type="checkbox" bind:checked={analyticsEnabled} />
|
<input type="checkbox" bind:checked={analyticsEnabled} />
|
||||||
<span>
|
<span>
|
||||||
<strong>Allow anonymous Aptabase events</strong>
|
<strong>{isGerman ? "Anonyme Aptabase-Ereignisse erlauben" : "Allow anonymous Aptabase events"}</strong>
|
||||||
<small>No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent.</small>
|
<small>{isGerman ? "Es werden keine Repository-Pfade, Remotes, Branches, Commit-Nachrichten, Dateinamen, Diffs, Zugangsdaten oder Code übertragen." : "No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent."}</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="new-branch-actions">
|
<div class="new-branch-actions">
|
||||||
<button class="btn-secondary" type="button" onclick={onClose}>Cancel</button>
|
<button class="btn-secondary" type="button" onclick={onClose}>{isGerman ? "Abbrechen" : "Cancel"}</button>
|
||||||
<button class="btn-primary" type="submit">
|
<button class="btn-primary" type="submit">
|
||||||
<Check size={16} aria-hidden="true" />
|
<Check size={16} aria-hidden="true" />
|
||||||
Save
|
{isGerman ? "Speichern" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,798 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, tick } from "svelte";
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
BookOpen,
|
||||||
|
Check,
|
||||||
|
ChevronRight,
|
||||||
|
CircleHelp,
|
||||||
|
Clipboard,
|
||||||
|
Cloud,
|
||||||
|
Command,
|
||||||
|
GitBranch,
|
||||||
|
GitCommitHorizontal,
|
||||||
|
Home,
|
||||||
|
Keyboard,
|
||||||
|
Lightbulb,
|
||||||
|
Search,
|
||||||
|
Wrench,
|
||||||
|
X,
|
||||||
|
} from "@lucide/svelte";
|
||||||
|
|
||||||
|
interface CommandExample {
|
||||||
|
command: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpSection {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
steps?: string[];
|
||||||
|
commands?: CommandExample[];
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpCategory {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
sections: HelpSection[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
language: "en" | "de";
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deCategories: HelpCategory[] = [
|
||||||
|
{
|
||||||
|
id: "start",
|
||||||
|
label: "Erste Schritte",
|
||||||
|
description: "Repository öffnen, Oberfläche verstehen und den ersten Commit erstellen.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "start-workflow",
|
||||||
|
title: "Dein erster Gitty-Workflow",
|
||||||
|
summary: "Vom Repository bis zum veröffentlichten Commit in fünf klaren Schritten.",
|
||||||
|
steps: [
|
||||||
|
"Öffne ein vorhandenes Repository oder klone ein Projekt über die Repository-Verwaltung.",
|
||||||
|
"Bearbeite deine Dateien. Gitty zeigt Änderungen im Arbeitsverzeichnis automatisch an.",
|
||||||
|
"Prüfe den Diff und stage einzelne Dateien, Zeilen oder alle passenden Änderungen.",
|
||||||
|
"Schreibe eine aussagekräftige Commit-Nachricht und erstelle den Commit.",
|
||||||
|
"Nutze Fetch, Pull und Push, um deinen Stand mit dem Remote-Repository abzugleichen.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "start-layout",
|
||||||
|
title: "Die Oberfläche im Überblick",
|
||||||
|
summary: "Links findest du Branches, Stashes und Dateien. In der Mitte prüfst und commitest du Änderungen; rechts siehst du Verlauf und Dateihistorie.",
|
||||||
|
note: "Fast alle Bereiche lassen sich über die Trennlinien in der Größe anpassen oder über ihren Kopf einklappen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "start-safety",
|
||||||
|
title: "Sicher arbeiten",
|
||||||
|
summary: "Prüfe vor Commit, Pull, Rebase oder Verwerfen immer den aktuellen Branch und die betroffenen Dateien. Gitty fragt bei destruktiven Aktionen nochmals nach.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app",
|
||||||
|
label: "Arbeiten mit Gitty",
|
||||||
|
description: "Die wichtigsten Funktionen der App Schritt für Schritt.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "app-changes",
|
||||||
|
title: "Änderungen prüfen und stagen",
|
||||||
|
summary: "Wähle eine geänderte Datei, lies den Diff und verschiebe gezielt Änderungen in den Staging-Bereich.",
|
||||||
|
steps: [
|
||||||
|
"Unstaged enthält noch nicht vorbereitete Änderungen; Staged enthält den nächsten Commit-Inhalt.",
|
||||||
|
"Öffne eine Datei, um hinzugefügte und entfernte Zeilen zu prüfen.",
|
||||||
|
"Stage ganze Dateien oder nutze die Zeilen-/Hunk-Aktionen für kleinere, saubere Commits.",
|
||||||
|
"Verwerfen entfernt lokale Änderungen. Diese Aktion lässt sich nicht immer rückgängig machen.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app-history",
|
||||||
|
title: "Verlauf, Suche und Vergleich",
|
||||||
|
summary: "Der Commit-Verlauf zeigt Branches und Merges. Mit der globalen Suche findest du die Einführung von Code oder den Verlauf einer Datei.",
|
||||||
|
steps: [
|
||||||
|
"Klappe einen Commit auf, um seine Dateien zu sehen.",
|
||||||
|
"Vergleiche zwei Commits über Compare in der Titelleiste.",
|
||||||
|
"Nutze Reflog, um auch verschobene oder nicht mehr sichtbare Referenzen wiederzufinden.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app-stash",
|
||||||
|
title: "Zwischenstände mit Stash sichern",
|
||||||
|
summary: "Ein Stash parkt unvollständige Änderungen, ohne einen Commit zu erzeugen. Apply behält den Stash, Pop wendet ihn an und entfernt ihn.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "basics",
|
||||||
|
label: "Git-Grundlagen",
|
||||||
|
description: "Alltägliche Befehle für Status, Änderungen, Commits und Wiederherstellung.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "basics-inspect",
|
||||||
|
title: "Repository prüfen",
|
||||||
|
summary: "Diese Befehle verändern nichts und eignen sich immer als erster Blick auf den aktuellen Zustand.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git status", description: "Arbeitsverzeichnis und Staging-Bereich anzeigen" },
|
||||||
|
{ command: "git diff", description: "Noch nicht gestagte Änderungen anzeigen" },
|
||||||
|
{ command: "git diff --staged", description: "Inhalt des nächsten Commits anzeigen" },
|
||||||
|
{ command: "git log --oneline --graph --decorate --all", description: "Kompakten Branch- und Commit-Verlauf anzeigen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "basics-commit",
|
||||||
|
title: "Änderungen speichern",
|
||||||
|
summary: "Stage nur zusammengehörige Änderungen und beschreibe im Commit, warum die Änderung nötig ist.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git add <datei>", description: "Eine Datei für den Commit vormerken" },
|
||||||
|
{ command: "git add -p", description: "Änderungen interaktiv und abschnittsweise stagen" },
|
||||||
|
{ command: "git commit -m \"Kurze Beschreibung\"", description: "Einen Commit erstellen" },
|
||||||
|
{ command: "git commit --amend", description: "Den letzten lokalen Commit ergänzen oder umbenennen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "basics-undo",
|
||||||
|
title: "Änderungen rückgängig machen",
|
||||||
|
summary: "Restore arbeitet am Arbeitsverzeichnis oder Staging-Bereich; Revert erzeugt einen neuen Gegen-Commit und ist für veröffentlichte Historie sicherer.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git restore <datei>", description: "Lokale, noch nicht gestagte Änderung verwerfen" },
|
||||||
|
{ command: "git restore --staged <datei>", description: "Datei aus dem Staging-Bereich entfernen" },
|
||||||
|
{ command: "git revert <commit>", description: "Wirkung eines Commits durch neuen Commit umkehren" },
|
||||||
|
],
|
||||||
|
note: "Vorsicht: git reset --hard verwirft lokale Änderungen. Verwende den Befehl nur, wenn du den Verlust bewusst akzeptierst.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "branches",
|
||||||
|
label: "Branches & Merges",
|
||||||
|
description: "Parallele Arbeit organisieren, zusammenführen und aufräumen.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "branches-manage",
|
||||||
|
title: "Branches verwalten",
|
||||||
|
summary: "Ein Branch ist ein beweglicher Zeiger auf eine Commit-Linie. Erstelle für jede getrennte Aufgabe einen eigenen Branch.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git switch -c feature/meine-aenderung", description: "Neuen Branch erstellen und wechseln" },
|
||||||
|
{ command: "git switch main", description: "Zu einem vorhandenen Branch wechseln" },
|
||||||
|
{ command: "git branch -vv", description: "Lokale Branches mit Upstream-Status anzeigen" },
|
||||||
|
{ command: "git branch -d <branch>", description: "Bereits zusammengeführten Branch löschen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "branches-integrate",
|
||||||
|
title: "Merge oder Rebase?",
|
||||||
|
summary: "Merge bewahrt die tatsächliche Verzweigung. Rebase setzt lokale Commits auf eine neue Basis und erzeugt eine lineare Historie.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git merge <branch>", description: "Einen Branch in den aktuellen Branch integrieren" },
|
||||||
|
{ command: "git rebase main", description: "Lokale Commits auf den aktuellen Stand von main setzen" },
|
||||||
|
{ command: "git rebase --continue", description: "Rebase nach gelöstem Konflikt fortsetzen" },
|
||||||
|
{ command: "git rebase --abort", description: "Rebase abbrechen und Ausgangszustand wiederherstellen" },
|
||||||
|
],
|
||||||
|
note: "Rebase keine Commits, an denen andere bereits weiterarbeiten. Das Umschreiben veröffentlichter Historie verursacht unnötige Konflikte.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "remote",
|
||||||
|
label: "Remote & Sync",
|
||||||
|
description: "Änderungen sicher mit GitHub, GitLab oder anderen Remotes austauschen.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "remote-sync",
|
||||||
|
title: "Fetch, Pull und Push",
|
||||||
|
summary: "Fetch lädt Referenzen ohne deine Dateien zu ändern. Pull integriert Remote-Änderungen. Push veröffentlicht deine Commits.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git fetch --all --prune", description: "Remote-Stände laden und entfernte Referenzen aufräumen" },
|
||||||
|
{ command: "git pull --rebase", description: "Remote-Änderungen laden und lokale Commits darauf neu abspielen" },
|
||||||
|
{ command: "git push -u origin <branch>", description: "Branch erstmals veröffentlichen und Upstream setzen" },
|
||||||
|
{ command: "git remote -v", description: "Konfigurierte Remote-Adressen anzeigen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "remote-ahead",
|
||||||
|
title: "Ahead und Behind verstehen",
|
||||||
|
summary: "Ahead bedeutet: lokale Commits wurden noch nicht gepusht. Behind bedeutet: im Remote liegen neue Commits, die lokal fehlen. Beides gleichzeitig weist auf auseinanderlaufende Historien hin.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "troubleshooting",
|
||||||
|
label: "Probleme lösen",
|
||||||
|
description: "Konflikte, verlorene Commits und typische Fehlermeldungen verstehen.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "trouble-conflicts",
|
||||||
|
title: "Merge-Konflikte lösen",
|
||||||
|
summary: "Ein Konflikt entsteht, wenn Git Änderungen nicht eindeutig kombinieren kann. Gitty bietet dafür einen eigenen Resolve-Dialog.",
|
||||||
|
steps: [
|
||||||
|
"Öffne jede Konfliktdatei und vergleiche Current, Incoming und das kombinierte Ergebnis.",
|
||||||
|
"Übernimm eine Seite oder bearbeite den Zielinhalt manuell.",
|
||||||
|
"Markiere die Datei als gelöst und prüfe anschließend den vollständigen Diff.",
|
||||||
|
"Führe Merge, Rebase oder Cherry-pick fort – oder brich die Operation vollständig ab.",
|
||||||
|
],
|
||||||
|
commands: [
|
||||||
|
{ command: "git status", description: "Konfliktdateien und laufende Operation anzeigen" },
|
||||||
|
{ command: "git merge --abort", description: "Laufenden Merge abbrechen" },
|
||||||
|
{ command: "git cherry-pick --abort", description: "Laufenden Cherry-pick abbrechen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trouble-reflog",
|
||||||
|
title: "Verlorene Commits wiederfinden",
|
||||||
|
summary: "Reflog protokolliert lokale Bewegungen von HEAD und Branches. Kopiere den Hash des gesuchten Eintrags und erstelle daraus einen Sicherungs-Branch.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git reflog", description: "Lokale Referenzbewegungen anzeigen" },
|
||||||
|
{ command: "git branch recovery/<name> <commit>", description: "Gefundenen Commit dauerhaft über Branch sichern" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "shortcuts",
|
||||||
|
label: "Tastenkürzel",
|
||||||
|
description: "Gitty schneller und ohne Maus bedienen.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "shortcuts-main",
|
||||||
|
title: "Globale Bedienung",
|
||||||
|
summary: "Die Hilfe ist überall erreichbar. Dialoge lassen sich konsistent schließen und Suchfelder direkt fokussieren.",
|
||||||
|
commands: [
|
||||||
|
{ command: "Ctrl + /", description: "Diese Hilfe öffnen" },
|
||||||
|
{ command: "Escape", description: "Aktuelles Overlay oder Dialogfenster schließen" },
|
||||||
|
{ command: "Tab / Shift + Tab", description: "Zwischen Bedienelementen wechseln" },
|
||||||
|
{ command: "Enter / Leertaste", description: "Fokussierte Aktion ausführen" },
|
||||||
|
],
|
||||||
|
note: "Auf macOS kannst du für Ctrl in der Regel die Command-Taste verwenden.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const enCategories: HelpCategory[] = [
|
||||||
|
{
|
||||||
|
id: "start",
|
||||||
|
label: "Getting started",
|
||||||
|
description: "Open a repository, understand the interface, and create your first commit.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "start-workflow",
|
||||||
|
title: "Your first Gitty workflow",
|
||||||
|
summary: "From repository to published commit in five clear steps.",
|
||||||
|
steps: [
|
||||||
|
"Open an existing repository or clone a project from Repository Management.",
|
||||||
|
"Edit your files. Gitty automatically displays working-tree changes.",
|
||||||
|
"Review the diff and stage individual files, lines, or all related changes.",
|
||||||
|
"Write a meaningful commit message and create the commit.",
|
||||||
|
"Use Fetch, Pull, and Push to synchronize with the remote repository.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "start-layout",
|
||||||
|
title: "Interface overview",
|
||||||
|
summary: "Branches, stashes, and files are on the left. Review and commit changes in the center; history and file history are on the right.",
|
||||||
|
note: "Most areas can be resized using their dividers or collapsed from their header.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "start-safety",
|
||||||
|
title: "Work safely",
|
||||||
|
summary: "Before committing, pulling, rebasing, or discarding, always check the current branch and affected files. Gitty confirms destructive actions.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app",
|
||||||
|
label: "Working with Gitty",
|
||||||
|
description: "The app's most important features, explained step by step.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "app-changes",
|
||||||
|
title: "Review and stage changes",
|
||||||
|
summary: "Select a changed file, review its diff, and move changes into the staging area.",
|
||||||
|
steps: [
|
||||||
|
"Unstaged contains changes not yet prepared; Staged contains the next commit.",
|
||||||
|
"Open a file to review added and removed lines.",
|
||||||
|
"Stage whole files or use line and hunk actions for focused commits.",
|
||||||
|
"Discard removes local changes and cannot always be undone.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app-history",
|
||||||
|
title: "History, search, and comparison",
|
||||||
|
summary: "Commit History shows branches and merges. Global Search finds where code was introduced or displays a file's history.",
|
||||||
|
steps: [
|
||||||
|
"Expand a commit to inspect its files.",
|
||||||
|
"Compare two commits with Compare in the title bar.",
|
||||||
|
"Use Reflog to recover moved or otherwise hidden references.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "app-stash",
|
||||||
|
title: "Save temporary work with Stash",
|
||||||
|
summary: "A stash parks unfinished changes without creating a commit. Apply keeps the stash; Pop applies and removes it.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "basics",
|
||||||
|
label: "Git basics",
|
||||||
|
description: "Everyday commands for status, changes, commits, and recovery.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "basics-inspect",
|
||||||
|
title: "Inspect a repository",
|
||||||
|
summary: "These commands do not change anything and are always a good first look at the current state.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git status", description: "Show the working tree and staging area" },
|
||||||
|
{ command: "git diff", description: "Show changes that have not been staged" },
|
||||||
|
{ command: "git diff --staged", description: "Show the contents of the next commit" },
|
||||||
|
{ command: "git log --oneline --graph --decorate --all", description: "Show a compact branch and commit graph" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "basics-commit",
|
||||||
|
title: "Save changes",
|
||||||
|
summary: "Stage only related changes and explain why the change is needed in the commit message.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git add <file>", description: "Stage one file" },
|
||||||
|
{ command: "git add -p", description: "Stage changes interactively by hunk" },
|
||||||
|
{ command: "git commit -m \"Short description\"", description: "Create a commit" },
|
||||||
|
{ command: "git commit --amend", description: "Update or rename the latest local commit" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "basics-undo",
|
||||||
|
title: "Undo changes",
|
||||||
|
summary: "Restore changes the working tree or staging area. Revert creates a new inverse commit and is safer for published history.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git restore <file>", description: "Discard an unstaged local change" },
|
||||||
|
{ command: "git restore --staged <file>", description: "Remove a file from the staging area" },
|
||||||
|
{ command: "git revert <commit>", description: "Undo a commit through a new commit" },
|
||||||
|
],
|
||||||
|
note: "Caution: git reset --hard discards local changes. Use it only when that data loss is intentional.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "branches",
|
||||||
|
label: "Branches & merges",
|
||||||
|
description: "Organize parallel work, integrate it, and clean up afterward.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "branches-manage",
|
||||||
|
title: "Manage branches",
|
||||||
|
summary: "A branch is a movable pointer to a commit line. Create a separate branch for each independent task.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git switch -c feature/my-change", description: "Create and switch to a new branch" },
|
||||||
|
{ command: "git switch main", description: "Switch to an existing branch" },
|
||||||
|
{ command: "git branch -vv", description: "Show local branches and upstream status" },
|
||||||
|
{ command: "git branch -d <branch>", description: "Delete a branch that has already been merged" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "branches-integrate",
|
||||||
|
title: "Merge or rebase?",
|
||||||
|
summary: "Merge preserves the actual branch structure. Rebase moves local commits onto a new base for a linear history.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git merge <branch>", description: "Integrate a branch into the current branch" },
|
||||||
|
{ command: "git rebase main", description: "Move local commits onto the latest main" },
|
||||||
|
{ command: "git rebase --continue", description: "Continue after resolving a conflict" },
|
||||||
|
{ command: "git rebase --abort", description: "Abort and restore the original state" },
|
||||||
|
],
|
||||||
|
note: "Do not rebase commits other people are already using. Rewriting published history creates avoidable conflicts.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "remote",
|
||||||
|
label: "Remote & sync",
|
||||||
|
description: "Exchange changes safely with GitHub, GitLab, or other remotes.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "remote-sync",
|
||||||
|
title: "Fetch, pull, and push",
|
||||||
|
summary: "Fetch downloads references without changing files. Pull integrates remote changes. Push publishes your commits.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git fetch --all --prune", description: "Download remote state and remove stale references" },
|
||||||
|
{ command: "git pull --rebase", description: "Download changes and replay local commits on top" },
|
||||||
|
{ command: "git push -u origin <branch>", description: "Publish a branch and set its upstream" },
|
||||||
|
{ command: "git remote -v", description: "Show configured remote URLs" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "remote-ahead",
|
||||||
|
title: "Understanding Ahead and Behind",
|
||||||
|
summary: "Ahead means local commits have not been pushed. Behind means remote commits are missing locally. Both at once means the histories have diverged.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "troubleshooting",
|
||||||
|
label: "Troubleshooting",
|
||||||
|
description: "Resolve conflicts, recover lost commits, and understand common errors.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "trouble-conflicts",
|
||||||
|
title: "Resolve merge conflicts",
|
||||||
|
summary: "A conflict occurs when Git cannot combine changes unambiguously. Gitty provides a dedicated Resolve dialog.",
|
||||||
|
steps: [
|
||||||
|
"Open each conflicted file and compare Current, Incoming, and the combined result.",
|
||||||
|
"Accept one side or edit the final content manually.",
|
||||||
|
"Mark the file resolved and review the complete diff.",
|
||||||
|
"Continue the merge, rebase, or cherry-pick—or abort the operation.",
|
||||||
|
],
|
||||||
|
commands: [
|
||||||
|
{ command: "git status", description: "Show conflicts and the active operation" },
|
||||||
|
{ command: "git merge --abort", description: "Abort the current merge" },
|
||||||
|
{ command: "git cherry-pick --abort", description: "Abort the current cherry-pick" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "trouble-reflog",
|
||||||
|
title: "Recover lost commits",
|
||||||
|
summary: "Reflog records local movements of HEAD and branches. Copy the desired hash and create a recovery branch from it.",
|
||||||
|
commands: [
|
||||||
|
{ command: "git reflog", description: "Show local reference movements" },
|
||||||
|
{ command: "git branch recovery/<name> <commit>", description: "Preserve the recovered commit with a branch" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "shortcuts",
|
||||||
|
label: "Keyboard shortcuts",
|
||||||
|
description: "Use Gitty quickly without reaching for the mouse.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "shortcuts-main",
|
||||||
|
title: "Global controls",
|
||||||
|
summary: "Help is available everywhere. Dialogs close consistently and search fields receive focus automatically.",
|
||||||
|
commands: [
|
||||||
|
{ command: "Ctrl + /", description: "Open this help center" },
|
||||||
|
{ command: "Escape", description: "Close the current overlay or dialog" },
|
||||||
|
{ command: "Tab / Shift + Tab", description: "Move between controls" },
|
||||||
|
{ command: "Enter / Space", description: "Activate the focused control" },
|
||||||
|
],
|
||||||
|
note: "On macOS, you can generally use Command instead of Ctrl.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let { language = "en", onClose = () => {} }: Props = $props();
|
||||||
|
const isGerman = $derived(language === "de");
|
||||||
|
const categories = $derived(isGerman ? deCategories : enCategories);
|
||||||
|
let selectedCategoryId = $state("start");
|
||||||
|
let searchQuery = $state("");
|
||||||
|
let copiedCommand = $state("");
|
||||||
|
let searchInput: HTMLInputElement;
|
||||||
|
let contentElement: HTMLElement;
|
||||||
|
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
|
const normalizedQuery = $derived(searchQuery.trim().toLocaleLowerCase(language));
|
||||||
|
const selectedCategory = $derived(categories.find((category) => category.id === selectedCategoryId) ?? categories[0]);
|
||||||
|
const visibleGroups = $derived.by(() => {
|
||||||
|
if (!normalizedQuery) return [{ category: selectedCategory, sections: selectedCategory.sections }];
|
||||||
|
return categories.flatMap((category) => {
|
||||||
|
const sections = category.sections.filter((section) => sectionSearchText(category, section).includes(normalizedQuery));
|
||||||
|
return sections.length > 0 ? [{ category, sections }] : [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const resultCount = $derived(visibleGroups.reduce((sum, group) => sum + group.sections.length, 0));
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
normalizedQuery;
|
||||||
|
selectedCategoryId;
|
||||||
|
if (contentElement) contentElement.scrollTop = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void tick().then(() => searchInput?.focus());
|
||||||
|
return () => {
|
||||||
|
if (copyTimer) clearTimeout(copyTimer);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function sectionSearchText(category: HelpCategory, section: HelpSection): string {
|
||||||
|
return [
|
||||||
|
category.label,
|
||||||
|
category.description,
|
||||||
|
section.title,
|
||||||
|
section.summary,
|
||||||
|
...(section.steps ?? []),
|
||||||
|
...(section.commands?.flatMap((entry) => [entry.command, entry.description]) ?? []),
|
||||||
|
section.note ?? "",
|
||||||
|
].join(" ").toLocaleLowerCase(language);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCategory(id: string) {
|
||||||
|
selectedCategoryId = id;
|
||||||
|
searchQuery = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWarningNote(note: string): boolean {
|
||||||
|
return /^(Vorsicht|Rebase|Caution|Do not)/.test(note);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyCommand(command: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(command);
|
||||||
|
copiedCommand = command;
|
||||||
|
if (copyTimer) clearTimeout(copyTimer);
|
||||||
|
copyTimer = setTimeout(() => { copiedCommand = ""; }, 1800);
|
||||||
|
} catch {
|
||||||
|
copiedCommand = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackdropClick(event: MouseEvent) {
|
||||||
|
if (event.target === event.currentTarget) onClose();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="help-backdrop" role="presentation" onclick={handleBackdropClick}>
|
||||||
|
<div class="help-overlay" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
||||||
|
<header class="help-header">
|
||||||
|
<div class="help-title-wrap">
|
||||||
|
<span class="help-mark"><CircleHelp size={19} aria-hidden="true" /></span>
|
||||||
|
<div>
|
||||||
|
<h2 id="help-title">{isGerman ? "Gitty Hilfe" : "Gitty Help"}</h2>
|
||||||
|
<p>{isGerman ? "App-Anleitung und Git-Wissen an einem Ort" : "App guidance and Git knowledge in one place"}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="help-search">
|
||||||
|
<span class="help-search-icon"><Search size={17} aria-hidden="true" /></span>
|
||||||
|
<input bind:this={searchInput} bind:value={searchQuery} type="search" placeholder={isGerman ? "Hilfe und Git-Befehle durchsuchen …" : "Search help and Git commands …"} aria-label={isGerman ? "Hilfe durchsuchen" : "Search help"} />
|
||||||
|
<kbd>Ctrl /</kbd>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button class="help-close" type="button" onclick={onClose} title={isGerman ? "Hilfe schließen" : "Close help"} aria-label={isGerman ? "Hilfe schließen" : "Close help"}>
|
||||||
|
<X size={19} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="help-layout">
|
||||||
|
<nav class="help-nav" aria-label={isGerman ? "Hilfethemen" : "Help topics"}>
|
||||||
|
<p class="help-nav-label">{isGerman ? "Themen" : "Topics"}</p>
|
||||||
|
{#each categories as category, index}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class:active={!normalizedQuery && category.id === selectedCategoryId}
|
||||||
|
onclick={() => selectCategory(category.id)}
|
||||||
|
>
|
||||||
|
<span class="help-nav-icon">
|
||||||
|
{#if index === 0}<Home size={17} aria-hidden="true" />
|
||||||
|
{:else if index === 1}<BookOpen size={17} aria-hidden="true" />
|
||||||
|
{:else if index === 2}<GitCommitHorizontal size={17} aria-hidden="true" />
|
||||||
|
{:else if index === 3}<GitBranch size={17} aria-hidden="true" />
|
||||||
|
{:else if index === 4}<Cloud size={17} aria-hidden="true" />
|
||||||
|
{:else if index === 5}<Wrench size={17} aria-hidden="true" />
|
||||||
|
{:else}<Keyboard size={17} aria-hidden="true" />{/if}
|
||||||
|
</span>
|
||||||
|
<span>{category.label}</span>
|
||||||
|
<span class="help-nav-chevron"><ChevronRight size={14} aria-hidden="true" /></span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="help-nav-tip">
|
||||||
|
<span class="help-tip-icon"><Lightbulb size={16} aria-hidden="true" /></span>
|
||||||
|
<span>{isGerman ? "Suche auch nach Befehlen wie" : "Try commands such as"} <code>rebase</code>, <code>stash</code> {isGerman ? "oder" : "or"} <code>reflog</code>.</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="help-content" bind:this={contentElement}>
|
||||||
|
{#if normalizedQuery}
|
||||||
|
<header class="help-result-header">
|
||||||
|
<div>
|
||||||
|
<span>{isGerman ? "Suchergebnisse" : "Search results"}</span>
|
||||||
|
<h3>{resultCount} {isGerman ? "Treffer für" : resultCount === 1 ? "result for" : "results for"} „{searchQuery.trim()}“</h3>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick={() => { searchQuery = ""; }}>{isGerman ? "Suche löschen" : "Clear search"}</button>
|
||||||
|
</header>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if visibleGroups.length === 0}
|
||||||
|
<div class="help-empty">
|
||||||
|
<span class="help-empty-icon"><Search size={28} aria-hidden="true" /></span>
|
||||||
|
<h3>{isGerman ? "Kein Hilfethema gefunden" : "No help topic found"}</h3>
|
||||||
|
<p>{isGerman ? "Versuche einen allgemeineren Begriff wie „Commit“, „Branch“, „Remote“ oder „Konflikt“." : "Try a broader term such as “commit”, “branch”, “remote”, or “conflict”."}</p>
|
||||||
|
<button class="btn-secondary" type="button" onclick={() => { searchQuery = ""; }}>{isGerman ? "Alle Themen anzeigen" : "Show all topics"}</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each visibleGroups as group}
|
||||||
|
<div class="help-group">
|
||||||
|
<header class="help-group-header">
|
||||||
|
<span>{normalizedQuery ? group.category.label : isGerman ? "Gitty Handbuch" : "Gitty handbook"}</span>
|
||||||
|
<h3>{normalizedQuery ? group.category.label : group.category.label}</h3>
|
||||||
|
<p>{group.category.description}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{#each group.sections as section, sectionIndex}
|
||||||
|
<article class="help-section" id={section.id}>
|
||||||
|
<div class="help-section-number">{String(sectionIndex + 1).padStart(2, "0")}</div>
|
||||||
|
<div class="help-section-body">
|
||||||
|
<h4>{section.title}</h4>
|
||||||
|
<p>{section.summary}</p>
|
||||||
|
|
||||||
|
{#if section.steps}
|
||||||
|
<ol class="help-steps">
|
||||||
|
{#each section.steps as step}
|
||||||
|
<li><span>{step}</span></li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if section.commands}
|
||||||
|
<div class="help-commands">
|
||||||
|
{#each section.commands as entry}
|
||||||
|
<div class="help-command-row">
|
||||||
|
<code>{entry.command}</code>
|
||||||
|
<span>{entry.description}</span>
|
||||||
|
<button type="button" onclick={() => copyCommand(entry.command)} aria-label={(isGerman ? "Befehl kopieren: " : "Copy command: ") + entry.command} title={isGerman ? "Befehl kopieren" : "Copy command"}>
|
||||||
|
{#if copiedCommand === entry.command}
|
||||||
|
<Check size={15} aria-hidden="true" /><span>{isGerman ? "Kopiert" : "Copied"}</span>
|
||||||
|
{:else}
|
||||||
|
<Clipboard size={15} aria-hidden="true" /><span>{isGerman ? "Kopieren" : "Copy"}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if section.note}
|
||||||
|
<div class="help-note" class:warning={isWarningNote(section.note)}>
|
||||||
|
{#if isWarningNote(section.note)}
|
||||||
|
<span class="help-note-icon"><AlertTriangle size={16} aria-hidden="true" /></span>
|
||||||
|
{:else}
|
||||||
|
<span class="help-note-icon"><Lightbulb size={16} aria-hidden="true" /></span>
|
||||||
|
{/if}
|
||||||
|
<span>{section.note}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="help-footer">
|
||||||
|
<span><Command size={14} aria-hidden="true" /> <kbd>Ctrl</kbd><kbd>/</kbd> {isGerman ? "Hilfe öffnen" : "open help"}</span>
|
||||||
|
<span><kbd>Esc</kbd> {isGerman ? "schließen" : "close"}</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.help-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: var(--app-titlebar-height, 42px) 0 0;
|
||||||
|
z-index: 70;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
background: rgba(3, 7, 13, 0.82);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-overlay {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||||
|
width: min(1240px, 100%);
|
||||||
|
height: min(820px, 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--color-ink);
|
||||||
|
background: var(--app-dialog-bg);
|
||||||
|
box-shadow: var(--app-dialog-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 0.72fr) minmax(320px, 1.2fr) minmax(44px, 0.72fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 22px;
|
||||||
|
min-height: 74px;
|
||||||
|
padding: 12px 16px 12px 20px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background: var(--app-dialog-chrome);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-title-wrap { display: flex; align-items: center; gap: 11px; min-width: 0; }
|
||||||
|
.help-mark { display: grid; place-items: center; width: 34px; height: 34px; border: 1px solid rgba(90, 140, 248, 0.28); border-radius: 8px; color: var(--color-accent); background: rgba(90, 140, 248, 0.09); }
|
||||||
|
.help-title-wrap h2 { margin: 0; font-size: 17px; line-height: 1.2; }
|
||||||
|
.help-title-wrap p { margin: 3px 0 0; overflow: hidden; color: var(--color-ink-faint); font-size: 10.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
|
.help-search { position: relative; display: flex; align-items: center; min-width: 0; }
|
||||||
|
.help-search-icon { position: absolute; left: 12px; display: grid; color: var(--color-ink-faint); pointer-events: none; }
|
||||||
|
.help-search input { width: 100%; height: 40px; padding: 0 68px 0 39px; border-color: var(--color-border-input); border-radius: 8px; background: var(--app-input-bg); color: var(--color-ink); font-size: 12.5px; }
|
||||||
|
.help-search kbd { position: absolute; right: 8px; }
|
||||||
|
.help-close { justify-self: end; width: 36px; min-height: 36px; padding: 0; border-color: transparent; border-radius: 7px; background: transparent; color: var(--color-ink-muted); }
|
||||||
|
|
||||||
|
.help-layout { display: grid; grid-template-columns: 250px minmax(0, 1fr); min-height: 0; }
|
||||||
|
.help-nav { display: flex; flex-direction: column; min-height: 0; padding: 14px 10px 12px; border-right: 1px solid var(--color-border); background: var(--color-surface-dim); }
|
||||||
|
.help-nav-label { margin: 0 10px 8px; color: var(--color-ink-faint); font-size: 9.5px; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; }
|
||||||
|
.help-nav > button { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 8px; width: 100%; min-height: 42px; padding: 5px 9px; border-color: transparent; border-radius: 7px; background: transparent; color: var(--color-ink-muted); font-size: 11.5px; font-weight: 650; text-align: left; }
|
||||||
|
.help-nav > button:hover { background: var(--color-surface-hover); color: var(--color-ink); }
|
||||||
|
.help-nav > button.active { border-color: rgba(90, 140, 248, 0.22); background: rgba(90, 140, 248, 0.12); color: var(--color-accent); }
|
||||||
|
.help-nav-icon { display: grid; place-items: center; color: currentColor; }
|
||||||
|
.help-nav-chevron { display: grid; color: var(--color-ink-faint); }
|
||||||
|
.help-nav-tip { display: flex; align-items: flex-start; gap: 8px; margin: auto 6px 0; padding: 11px; border-top: 1px solid var(--color-border-subtle); color: var(--color-ink-faint); font-size: 10.5px; line-height: 1.5; }
|
||||||
|
.help-tip-icon { flex: 0 0 auto; display: grid; margin-top: 2px; color: #d8a13d; }
|
||||||
|
.help-nav-tip code { color: var(--color-ink-muted); font-family: var(--font-mono); }
|
||||||
|
|
||||||
|
.help-content { min-width: 0; min-height: 0; padding: 0 34px 48px; overflow: auto; outline: none; scroll-behavior: smooth; }
|
||||||
|
.help-group { max-width: 850px; margin: 0 auto; }
|
||||||
|
.help-group + .help-group { margin-top: 22px; border-top: 1px solid var(--color-border); }
|
||||||
|
.help-group-header { padding: 34px 0 26px; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.help-group-header > span, .help-result-header span { color: var(--color-accent); font-size: 9.5px; font-weight: 800; letter-spacing: 0.11em; text-transform: uppercase; }
|
||||||
|
.help-group-header h3 { margin: 7px 0 8px; font-size: clamp(24px, 3vw, 32px); letter-spacing: -0.025em; }
|
||||||
|
.help-group-header p { max-width: 650px; margin: 0; color: var(--color-ink-muted); font-size: 13px; line-height: 1.55; }
|
||||||
|
|
||||||
|
.help-result-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; max-width: 850px; margin: 0 auto; padding: 22px 0 0; }
|
||||||
|
.help-result-header h3 { margin: 4px 0 0; font-size: 17px; }
|
||||||
|
.help-result-header button { min-height: 30px; padding: 0 10px; border-color: var(--color-border-subtle); background: transparent; color: var(--color-ink-muted); font-size: 10.5px; }
|
||||||
|
|
||||||
|
.help-section { display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 2px; padding: 26px 0; border-bottom: 1px solid var(--color-border-subtle); }
|
||||||
|
.help-section-number { padding-top: 3px; color: var(--color-ink-faint); font-family: var(--font-mono); font-size: 10px; }
|
||||||
|
.help-section-body h4 { margin: 0 0 7px; font-size: 16px; }
|
||||||
|
.help-section-body > p { margin: 0; color: var(--color-ink-muted); font-size: 12.5px; line-height: 1.6; }
|
||||||
|
.help-steps { display: grid; gap: 10px; margin: 18px 0 0; padding: 0; list-style: none; counter-reset: help-step; }
|
||||||
|
.help-steps li { display: grid; grid-template-columns: 24px minmax(0, 1fr); align-items: start; gap: 9px; color: var(--color-ink-muted); font-size: 12px; line-height: 1.5; counter-increment: help-step; }
|
||||||
|
.help-steps li::before { content: counter(help-step); display: grid; place-items: center; width: 22px; height: 22px; border: 1px solid rgba(90, 140, 248, 0.35); border-radius: 50%; color: var(--color-accent); font-family: var(--font-mono); font-size: 9px; font-weight: 800; }
|
||||||
|
|
||||||
|
.help-commands { display: grid; gap: 1px; margin-top: 17px; overflow: hidden; border: 1px solid var(--color-border); border-radius: 8px; background: var(--color-border-subtle); }
|
||||||
|
.help-command-row { display: grid; grid-template-columns: minmax(210px, 0.9fr) minmax(190px, 1.2fr) auto; align-items: center; gap: 14px; min-height: 47px; padding: 6px 7px 6px 13px; background: var(--code-surface); }
|
||||||
|
.help-command-row:hover { background: var(--code-hover-bg); }
|
||||||
|
.help-command-row > code { overflow: hidden; color: var(--color-accent); font-family: var(--font-mono); font-size: 11.5px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.help-command-row > span { color: var(--color-ink-faint); font-size: 10.5px; line-height: 1.35; }
|
||||||
|
.help-command-row button { display: inline-flex; align-items: center; gap: 6px; min-width: 83px; min-height: 31px; padding: 0 9px; border-color: var(--color-border-subtle); border-radius: 6px; background: var(--color-surface-raised); color: var(--color-ink-muted); font-size: 10px; }
|
||||||
|
.help-command-row button:hover { border-color: rgba(90, 140, 248, 0.32); color: var(--color-accent); }
|
||||||
|
|
||||||
|
.help-note { display: flex; align-items: flex-start; gap: 9px; margin-top: 15px; padding: 11px 12px; border: 1px solid rgba(90, 140, 248, 0.2); border-radius: 7px; background: rgba(90, 140, 248, 0.07); color: var(--color-ink-muted); font-size: 11px; line-height: 1.5; }
|
||||||
|
.help-note-icon { flex: 0 0 auto; display: grid; margin-top: 1px; color: var(--color-accent); }
|
||||||
|
.help-note.warning { border-color: rgba(224, 160, 64, 0.24); background: rgba(224, 160, 64, 0.07); }
|
||||||
|
.help-note.warning .help-note-icon { color: #d8a13d; }
|
||||||
|
|
||||||
|
.help-empty { display: grid; justify-items: center; max-width: 520px; margin: 90px auto 0; text-align: center; }
|
||||||
|
.help-empty-icon { display: grid; color: var(--color-ink-faint); }
|
||||||
|
.help-empty h3 { margin: 14px 0 5px; font-size: 18px; }
|
||||||
|
.help-empty p { margin: 0 0 18px; color: var(--color-ink-muted); font-size: 12px; line-height: 1.55; }
|
||||||
|
|
||||||
|
.help-footer { display: flex; align-items: center; justify-content: space-between; min-height: 38px; padding: 0 16px 0 20px; border-top: 1px solid var(--color-border); background: var(--app-dialog-chrome); color: var(--color-ink-faint); font-size: 9.5px; }
|
||||||
|
.help-footer span { display: inline-flex; align-items: center; gap: 5px; }
|
||||||
|
kbd { display: inline-grid; place-items: center; min-width: 20px; height: 21px; padding: 0 5px; border: 1px solid var(--color-border-input); border-radius: 5px; background: var(--color-surface-raised); color: var(--color-ink-muted); box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.04); font-family: var(--font-mono); font-size: 9px; }
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.help-backdrop { padding: 10px; }
|
||||||
|
.help-header { grid-template-columns: minmax(0, 1fr) auto; gap: 10px; }
|
||||||
|
.help-title-wrap { display: none; }
|
||||||
|
.help-layout { grid-template-columns: 190px minmax(0, 1fr); }
|
||||||
|
.help-content { padding-inline: 20px; }
|
||||||
|
.help-command-row { grid-template-columns: minmax(0, 1fr) auto; gap: 5px 10px; }
|
||||||
|
.help-command-row > span { grid-column: 1; }
|
||||||
|
.help-command-row button { grid-column: 2; grid-row: 1 / 3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.help-layout { grid-template-columns: 1fr; }
|
||||||
|
.help-nav { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.help-nav-label, .help-nav-tip, .help-nav-chevron { display: none; }
|
||||||
|
.help-nav > button { flex: 0 0 auto; width: auto; grid-template-columns: 24px auto; }
|
||||||
|
.help-content { padding-inline: 15px; }
|
||||||
|
.help-section { grid-template-columns: 1fr; }
|
||||||
|
.help-section-number { display: none; }
|
||||||
|
.help-footer { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,6 +11,7 @@ export type CommitAiPhase = "idle" | "loading" | "ready" | "error";
|
|||||||
export type CommitAiProvider = "local" | "openai" | "anthropic" | "custom";
|
export type CommitAiProvider = "local" | "openai" | "anthropic" | "custom";
|
||||||
export type CommitAiLocalProfile = "fast" | "balanced" | "detailed";
|
export type CommitAiLocalProfile = "fast" | "balanced" | "detailed";
|
||||||
export type AppTheme = "system" | "light" | "dark";
|
export type AppTheme = "system" | "light" | "dark";
|
||||||
|
export type AppLanguage = "en" | "de";
|
||||||
|
|
||||||
export interface CommitAiStatus {
|
export interface CommitAiStatus {
|
||||||
phase: CommitAiPhase;
|
phase: CommitAiPhase;
|
||||||
|
|||||||
Reference in New Issue
Block a user