Files
openchamber/packages/ui/src/components/views/git/StashDialog.tsx
T
Bohdan Triapitsyn 14357257ae perf(ui): migrate icons to SVG sprite system
Replace @remixicon/react with a shared Icon component that renders
via <use href> references to a single hidden SVG sprite. This reduces
DOM node count by replacing inline SVGs with lightweight references.

- Create Icon component with sprite injection (packages/ui/src/components/icon/)
- Migrate all 164 files from @remixicon/react to Icon component
- Auto-generate sprite data from remixicon bundle (scripts/generate-icon-sprite.mjs)
- Add bun run icons:generate to package.json
- Move @remixicon/react to devDependencies
- Add icon usage instructions to theme-system skill
2026-05-13 13:26:35 +03:00

130 lines
4.0 KiB
TypeScript

import React from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/components/ui';
import { Icon } from "@/components/icon/Icon";
import { useI18n } from '@/lib/i18n';
interface StashDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
operation: 'merge' | 'rebase';
targetBranch: string;
onConfirm: (restoreAfter: boolean) => Promise<void>;
}
export const StashDialog: React.FC<StashDialogProps> = ({
open,
onOpenChange,
operation,
targetBranch,
onConfirm,
}) => {
const { t } = useI18n();
const [restoreAfter, setRestoreAfter] = React.useState(true);
const [isProcessing, setIsProcessing] = React.useState(false);
const operationLabel = operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase');
const handleConfirm = async () => {
setIsProcessing(true);
try {
await onConfirm(restoreAfter);
onOpenChange(false);
} catch (err) {
// Show error to user - parent may also handle it but user should see feedback
const message = err instanceof Error ? err.message : `Failed to ${operation}`;
toast.error(message);
} finally {
setIsProcessing(false);
}
};
const handleCancel = () => {
if (!isProcessing) {
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={isProcessing ? undefined : onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<div className="flex items-center gap-2">
<Icon name="alert" className="size-5 text-[var(--status-warning)]" />
<DialogTitle>{t('gitView.stash.title')}</DialogTitle>
</div>
<DialogDescription>
{t('gitView.stash.description', { operation })}
</DialogDescription>
</DialogHeader>
<div className="py-2">
<p className="typography-meta text-muted-foreground mb-3">
{t('gitView.stash.thisWill')}
</p>
<ol className="list-decimal list-inside space-y-1 typography-meta text-foreground">
<li>{t('gitView.stash.stepStash')}</li>
<li>
{operation === 'merge' ? t('gitView.operation.merge') : t('gitView.operation.rebase')}{' '}
{operation === 'merge' ? t('gitView.stash.mergeWith') : t('gitView.stash.rebaseOnto')}{' '}
<span className="font-mono text-primary">{targetBranch}</span>
</li>
{restoreAfter && <li>{t('gitView.stash.stepRestore')}</li>}
</ol>
</div>
<div className="flex items-center gap-2 py-2">
<Checkbox
checked={restoreAfter}
onChange={setRestoreAfter}
disabled={isProcessing}
ariaLabel={t('gitView.stash.restoreAria')}
/>
<span
className="typography-ui-label text-foreground cursor-pointer select-none"
onClick={() => !isProcessing && setRestoreAfter(!restoreAfter)}
>
{t('gitView.stash.restoreAfterOperation', { operation })}
</span>
</div>
<DialogFooter className="gap-2">
<Button
variant="ghost"
size="sm"
onClick={handleCancel}
disabled={isProcessing}
>
{t('gitView.common.cancel')}
</Button>
<Button
variant="default"
size="sm"
onClick={handleConfirm}
disabled={isProcessing}
className="gap-1.5"
>
{isProcessing ? (
<>
<Icon name="loader-4" className="size-4 animate-spin" />
{t('gitView.common.processing')}
</>
) : (
t('gitView.stash.confirmButton', { operation: operationLabel })
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};