Files
openchamber/packages/ui/src/apps/MobileProjectEditSurface.tsx
T
00821700de chore: remove dead code (59 unused files + ~125 unused exports) (#1835)
* chore: remove dead/unreferenced files across ui, vscode

Remove 59 unused source files (components, hooks, lib utils, stores,
barrels, and orphaned vscode github modules) that are not imported by
any entry-reachable code. Also drop a stale test mock for the removed
execCommands module.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove unused exported symbols (types, functions, consts, hooks)

Remove exported symbols whose identifier is referenced nowhere in the
repository (verified via repo-wide search), across ui types/contracts,
lib utilities, sync layer, stores, and components. Also drop the few
imports/private helpers orphaned by these removals.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* refactor: remove more unused exports (desktop, shortcuts, worktree, vscode)

Continue removing repo-wide unreferenced exported functions, consts and
types across lib/desktop, shortcuts, worktreeSessionCreator, sync, and
vscode gitService, with cascading orphaned helpers/imports cleaned up.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* chore: add dead-code cleanup tooling

* refactor: checkpoint dead-code cleanup

* refactor: remove dead-code suppressions

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 19:27:53 +03:00

424 lines
17 KiB
TypeScript

import React from 'react';
import { RiCheckLine, RiDeleteBinLine, RiDragMove2Line, RiFolder6Line } from '@remixicon/react';
import {
DndContext,
type DragEndEvent,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Icon } from '@/components/icon/Icon';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui';
import { useThemeSystem } from '@/contexts/useThemeSystem';
import { useI18n } from '@/lib/i18n';
import { PROJECT_COLORS, PROJECT_COLOR_MAP, PROJECT_ICONS, PROJECT_ICON_MAP, ProjectIconImage } from '@/lib/projectMeta';
import { cn } from '@/lib/utils';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useWorktreeOrderStore } from '@/stores/useWorktreeOrderStore';
import type { WorktreeMetadata } from '@/types/worktree';
import { MobileDeleteWorktreeDialog } from './MobileDeleteWorktreeDialog';
import { MobileSurfaceShell } from './MobileSurfaceShell';
type MobileEditableProject = {
id: string;
label: string;
path: string;
icon?: string | null;
color?: string | null;
iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' } | null;
iconBackground?: string | null;
isGitRepo: boolean;
worktrees: WorktreeMetadata[];
};
type MobileProjectEditSurfaceProps = {
open: boolean;
project: MobileEditableProject | null;
onClose: () => void;
/** Called after a worktree is deleted so the parent can re-list worktrees. */
onWorktreesChanged?: () => void;
};
const normalizePath = (value?: string | null): string =>
(value || '').replace(/\\/g, '/').replace(/\/+$/, '');
const SortableWorktreeRow: React.FC<{
worktree: WorktreeMetadata;
onDelete: () => void;
}> = ({ worktree, onDelete }) => {
const { t } = useI18n();
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: worktree.path });
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 10 : 1,
};
const label = worktree.branch || worktree.label || worktree.path;
return (
<div
ref={setNodeRef}
style={style}
className={cn(
'flex items-center gap-1 rounded-2xl border border-border/40 bg-[var(--surface-elevated)] px-1.5 py-1.5 transition-colors',
isDragging && 'shadow-lg shadow-black/20',
)}
>
<button
type="button"
className="flex size-9 shrink-0 cursor-grab touch-none items-center justify-center rounded-xl text-muted-foreground/70 transition-colors hover:text-foreground active:cursor-grabbing"
aria-label={t('mobile.projectEdit.dragWorktreeAria', { label })}
{...attributes}
{...listeners}
>
<RiDragMove2Line className="size-4" />
</button>
<Icon name="node-tree" className="size-4 shrink-0 text-muted-foreground" />
<span className="block min-w-0 flex-1 truncate typography-ui-label text-foreground">{label}</span>
<button
type="button"
className="flex size-9 shrink-0 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive"
aria-label={t('mobile.projectEdit.deleteWorktreeAria', { label })}
onClick={onDelete}
style={{ touchAction: 'manipulation' }}
>
<RiDeleteBinLine className="size-4" />
</button>
</div>
);
};
export const MobileProjectEditSurface: React.FC<MobileProjectEditSurfaceProps> = ({
open,
project,
onClose,
onWorktreesChanged,
}) => {
const { t } = useI18n();
const { currentTheme } = useThemeSystem();
const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta);
const discoverProjectIcon = useProjectsStore((state) => state.discoverProjectIcon);
const removeProjectIcon = useProjectsStore((state) => state.removeProjectIcon);
const setWorktreeOrder = useWorktreeOrderStore((state) => state.setWorktreeOrder);
// Read the live icon image from the store so discover/remove reflect instantly.
const currentIconImage = useProjectsStore((state) =>
project ? state.projects.find((entry) => entry.id === project.id)?.iconImage ?? null : null,
);
const [name, setName] = React.useState('');
const [icon, setIcon] = React.useState<string | null>(null);
const [color, setColor] = React.useState<string | null>(null);
const [isDiscovering, setIsDiscovering] = React.useState(false);
const [orderedWorktrees, setOrderedWorktrees] = React.useState<WorktreeMetadata[]>([]);
const [worktreeToDelete, setWorktreeToDelete] = React.useState<WorktreeMetadata | null>(null);
const projectId = project?.id ?? null;
React.useEffect(() => {
if (!open || !project) return;
setName(project.label);
setIcon(project.icon ?? null);
setColor(project.color ?? null);
setOrderedWorktrees(project.worktrees);
// Re-seed only when the edited project or sheet visibility changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, projectId]);
// Keep the worktree list in sync as the underlying project data updates
// (e.g. after a deletion), without clobbering an in-progress drag order.
React.useEffect(() => {
if (!open || !project) return;
setOrderedWorktrees((previous) => {
const incomingPaths = project.worktrees.map((worktree) => worktree.path);
const previousPaths = previous.map((worktree) => worktree.path);
const sameSet =
incomingPaths.length === previousPaths.length &&
incomingPaths.every((path) => previousPaths.includes(path));
return sameSet ? previous : project.worktrees;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project?.worktrees]);
const dndSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
const handleSave = () => {
if (!project) return;
const trimmed = name.trim();
updateProjectMeta(project.id, {
label: trimmed || project.label,
icon,
color,
});
onClose();
};
const handleDiscoverIcon = () => {
if (!project || isDiscovering) return;
setIsDiscovering(true);
void discoverProjectIcon(project.id)
.then((result) => {
if (!result.ok) {
toast.error(result.error || t('projectEditDialog.toast.failedToDiscoverIcon'));
return;
}
if (result.skipped) {
toast.success(t('projectEditDialog.toast.customIconAlreadySet'));
return;
}
toast.success(t('projectEditDialog.toast.iconDiscovered'));
})
.finally(() => setIsDiscovering(false));
};
const handleRemoveDiscoveredIcon = () => {
if (!project) return;
void removeProjectIcon(project.id).then((result) => {
if (!result.ok) {
toast.error(result.error || t('projectEditDialog.toast.failedToRemoveIcon'));
return;
}
toast.success(t('projectEditDialog.toast.iconRemoved'));
});
};
const handleWorktreeDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id || !project) return;
const fromIndex = orderedWorktrees.findIndex((worktree) => worktree.path === active.id);
const toIndex = orderedWorktrees.findIndex((worktree) => worktree.path === over.id);
if (fromIndex < 0 || toIndex < 0) return;
const next = [...orderedWorktrees];
const [moved] = next.splice(fromIndex, 1);
next.splice(toIndex, 0, moved);
setOrderedWorktrees(next);
setWorktreeOrder(
project.id,
next.map((worktree) => normalizePath(worktree.path)),
);
};
const currentColorVar = color ? PROJECT_COLOR_MAP[color] ?? null : null;
const previewIconName = icon ? PROJECT_ICON_MAP[icon] : null;
const hasImageIcon = Boolean(currentIconImage);
return (
<>
<MobileSurfaceShell
open={open}
onClose={onClose}
onBack={onClose}
title={t('projectEditDialog.title')}
ariaLabel={t('projectEditDialog.title')}
trailing={
<Button
type="button"
variant="default"
size="sm"
aria-label={t('projectEditDialog.actions.save')}
onClick={handleSave}
disabled={!name.trim()}
style={{ touchAction: 'manipulation' }}
>
<RiCheckLine className="size-4" />
{t('projectEditDialog.actions.save')}
</Button>
}
>
{project ? (
<div className="h-full space-y-6 overflow-y-auto px-4 pb-8 pt-2">
{/* Icon preview */}
<div className="flex justify-center pt-2">
<span
className="flex size-16 items-center justify-center overflow-hidden rounded-2xl bg-[var(--surface-muted)] text-muted-foreground"
style={project.iconBackground ? { backgroundColor: project.iconBackground } : undefined}
>
{hasImageIcon ? (
<ProjectIconImage
project={{ id: project.id, iconImage: currentIconImage }}
options={{
themeVariant: currentTheme.metadata.variant,
iconColor: currentTheme.colors.surface.foreground,
}}
className="size-full object-contain"
fallback={<RiFolder6Line className="size-7" />}
/>
) : previewIconName ? (
<Icon name={previewIconName} className="size-7" style={currentColorVar ? { color: currentColorVar } : undefined} />
) : (
<RiFolder6Line className="size-7" style={currentColorVar ? { color: currentColorVar } : undefined} />
)}
</span>
</div>
{/* Name */}
<div className="flex flex-col gap-1.5">
<label className="typography-ui-label font-medium text-foreground">
{t('projectEditDialog.field.name')}
</label>
<Input
value={name}
onChange={(event) => setName(event.target.value)}
placeholder={t('projectEditDialog.field.namePlaceholder')}
className="h-11"
/>
<p className="truncate typography-meta text-muted-foreground" title={project.path}>
{project.path}
</p>
</div>
{/* Color */}
<div className="flex flex-col gap-2">
<label className="typography-ui-label font-medium text-foreground">
{t('projectEditDialog.field.color')}
</label>
<div className="flex flex-wrap gap-2.5">
<button
type="button"
onClick={() => setColor(null)}
aria-label={t('projectEditDialog.option.none')}
className={cn(
'flex size-9 items-center justify-center rounded-xl border-2 transition-all',
color === null ? 'border-foreground' : 'border-border hover:border-border/80',
)}
style={{ touchAction: 'manipulation' }}
>
<span className="h-0.5 w-4 rotate-45 rounded-full bg-muted-foreground/40" />
</button>
{PROJECT_COLORS.map((c) => (
<button
key={c.key}
type="button"
onClick={() => setColor(c.key)}
aria-label={c.label}
title={c.label}
className={cn(
'size-9 rounded-xl border-2 transition-all',
color === c.key ? 'border-foreground' : 'border-transparent hover:border-border',
)}
style={{ backgroundColor: c.cssVar, touchAction: 'manipulation' }}
/>
))}
</div>
</div>
{/* Icon */}
<div className="flex flex-col gap-2">
<label className="typography-ui-label font-medium text-foreground">
{t('projectEditDialog.field.icon')}
</label>
<div className="flex flex-wrap gap-2.5">
<button
type="button"
onClick={() => setIcon(null)}
aria-label={t('projectEditDialog.option.none')}
className={cn(
'flex size-9 items-center justify-center rounded-xl border-2 transition-all',
icon === null ? 'border-foreground bg-[var(--surface-elevated)]' : 'border-border hover:border-border/80',
)}
style={{ touchAction: 'manipulation' }}
>
<span className="h-0.5 w-4 rotate-45 rounded-full bg-muted-foreground/40" />
</button>
{PROJECT_ICONS.map((i) => (
<button
key={i.key}
type="button"
onClick={() => setIcon(i.key)}
aria-label={i.label}
title={i.label}
className={cn(
'flex size-9 items-center justify-center rounded-xl border-2 transition-all',
icon === i.key ? 'border-foreground bg-[var(--surface-elevated)]' : 'border-border hover:border-border/80',
)}
style={{ touchAction: 'manipulation' }}
>
<Icon name={i.Icon} className="size-4" style={currentColorVar ? { color: currentColorVar } : undefined} />
</button>
))}
</div>
<div className="flex flex-wrap items-center gap-2 pt-1">
<Button size="sm" variant="outline" onClick={handleDiscoverIcon} disabled={isDiscovering}>
{isDiscovering
? t('projectEditDialog.actions.discovering')
: t('projectEditDialog.actions.discoverFavicon')}
</Button>
{hasImageIcon ? (
<Button size="sm" variant="outline" onClick={handleRemoveDiscoveredIcon}>
{t('projectEditDialog.actions.removeProjectIcon')}
</Button>
) : null}
</div>
</div>
{/* Worktrees */}
{project.isGitRepo ? (
<div className="flex flex-col gap-2">
<label className="typography-ui-label font-medium text-foreground">
{t('mobile.projectEdit.worktreesTitle')}
</label>
{orderedWorktrees.length === 0 ? (
<p className="typography-meta text-muted-foreground">
{t('mobile.projectEdit.worktreesEmpty')}
</p>
) : (
<>
<p className="typography-meta text-muted-foreground">
{t('mobile.projectEdit.reorderHint')}
</p>
<DndContext sensors={dndSensors} collisionDetection={closestCenter} onDragEnd={handleWorktreeDragEnd}>
<SortableContext
items={orderedWorktrees.map((worktree) => worktree.path)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col gap-1.5">
{orderedWorktrees.map((worktree) => (
<SortableWorktreeRow
key={worktree.path}
worktree={worktree}
onDelete={() => setWorktreeToDelete(worktree)}
/>
))}
</div>
</SortableContext>
</DndContext>
</>
)}
</div>
) : null}
</div>
) : null}
</MobileSurfaceShell>
{project ? (
<MobileDeleteWorktreeDialog
open={Boolean(worktreeToDelete)}
project={{ id: project.id, path: project.path }}
worktree={worktreeToDelete}
onClose={() => setWorktreeToDelete(null)}
onDeleted={() => {
setOrderedWorktrees((previous) =>
previous.filter((worktree) => worktree.path !== worktreeToDelete?.path),
);
onWorktreesChanged?.();
}}
/>
) : null}
</>
);
};