mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: enhance BottomDock and GitHubPanel with new state management and validation features
- Integrated keyboard shortcuts into BottomDock for improved accessibility and navigation. - Refactored dock state management to include expanded and maximized states, allowing for more dynamic UI behavior. - Added issue validation functionality in GitHubPanel, enabling users to validate GitHub issues with real-time feedback. - Implemented a validation dialog for displaying results and managing issue validation status. These enhancements significantly improve user interaction and functionality within the application, particularly in managing dock states and GitHub issues.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useCallback, useSyncExternalStore, useRef, useEffect } from 'react';
|
import { useState, useCallback, useSyncExternalStore, useRef, useEffect } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { useKeyboardShortcuts, useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Terminal,
|
Terminal,
|
||||||
@@ -35,53 +36,76 @@ export type DockPosition = 'bottom' | 'right' | 'left';
|
|||||||
|
|
||||||
const DOCK_POSITION_STORAGE_KEY = 'automaker:dock-position';
|
const DOCK_POSITION_STORAGE_KEY = 'automaker:dock-position';
|
||||||
|
|
||||||
// Event emitter for dock position changes
|
// Event emitter for dock state changes
|
||||||
const positionListeners = new Set<() => void>();
|
const stateListeners = new Set<() => void>();
|
||||||
|
|
||||||
function emitPositionChange() {
|
function emitStateChange() {
|
||||||
positionListeners.forEach((listener) => listener());
|
stateListeners.forEach((listener) => listener());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cached position to avoid creating new objects on every read
|
// Cached dock state
|
||||||
let cachedPosition: DockPosition = 'bottom';
|
interface DockState {
|
||||||
|
position: DockPosition;
|
||||||
|
isExpanded: boolean;
|
||||||
|
isMaximized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize from localStorage
|
let cachedState: DockState = {
|
||||||
|
position: 'bottom',
|
||||||
|
isExpanded: false,
|
||||||
|
isMaximized: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize position from localStorage
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(DOCK_POSITION_STORAGE_KEY) as DockPosition | null;
|
const stored = localStorage.getItem(DOCK_POSITION_STORAGE_KEY) as DockPosition | null;
|
||||||
if (stored && ['bottom', 'right', 'left'].includes(stored)) {
|
if (stored && ['bottom', 'right', 'left'].includes(stored)) {
|
||||||
cachedPosition = stored;
|
cachedState.position = stored;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore localStorage errors
|
// Ignore localStorage errors
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPosition(): DockPosition {
|
function getDockState(): DockState {
|
||||||
return cachedPosition;
|
return cachedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePosition(position: DockPosition) {
|
function updatePosition(position: DockPosition) {
|
||||||
if (cachedPosition !== position) {
|
if (cachedState.position !== position) {
|
||||||
cachedPosition = position;
|
cachedState = { ...cachedState, position };
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(DOCK_POSITION_STORAGE_KEY, position);
|
localStorage.setItem(DOCK_POSITION_STORAGE_KEY, position);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore localStorage errors
|
// Ignore localStorage errors
|
||||||
}
|
}
|
||||||
emitPositionChange();
|
emitStateChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook for external components to read dock position
|
function updateExpanded(isExpanded: boolean) {
|
||||||
export function useDockState(): { position: DockPosition } {
|
if (cachedState.isExpanded !== isExpanded) {
|
||||||
const position = useSyncExternalStore(
|
cachedState = { ...cachedState, isExpanded };
|
||||||
|
emitStateChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMaximized(isMaximized: boolean) {
|
||||||
|
if (cachedState.isMaximized !== isMaximized) {
|
||||||
|
cachedState = { ...cachedState, isMaximized };
|
||||||
|
emitStateChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook for external components to read dock state
|
||||||
|
export function useDockState(): DockState {
|
||||||
|
return useSyncExternalStore(
|
||||||
(callback) => {
|
(callback) => {
|
||||||
positionListeners.add(callback);
|
stateListeners.add(callback);
|
||||||
return () => positionListeners.delete(callback);
|
return () => stateListeners.delete(callback);
|
||||||
},
|
},
|
||||||
getPosition,
|
getDockState,
|
||||||
getPosition
|
getDockState
|
||||||
);
|
);
|
||||||
return { position };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BottomDockProps {
|
interface BottomDockProps {
|
||||||
@@ -98,13 +122,22 @@ export function BottomDock({ className }: BottomDockProps) {
|
|||||||
// Use external store for position - single source of truth
|
// Use external store for position - single source of truth
|
||||||
const position = useSyncExternalStore(
|
const position = useSyncExternalStore(
|
||||||
(callback) => {
|
(callback) => {
|
||||||
positionListeners.add(callback);
|
stateListeners.add(callback);
|
||||||
return () => positionListeners.delete(callback);
|
return () => stateListeners.delete(callback);
|
||||||
},
|
},
|
||||||
getPosition,
|
() => getDockState().position,
|
||||||
getPosition
|
() => getDockState().position
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Sync local expanded/maximized state to external store for other components
|
||||||
|
useEffect(() => {
|
||||||
|
updateExpanded(isExpanded);
|
||||||
|
}, [isExpanded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateMaximized(isMaximized);
|
||||||
|
}, [isMaximized]);
|
||||||
|
|
||||||
const autoModeState = currentProject ? getAutoModeState(currentProject.id) : null;
|
const autoModeState = currentProject ? getAutoModeState(currentProject.id) : null;
|
||||||
const runningAgentsCount = autoModeState?.runningTasks?.length ?? 0;
|
const runningAgentsCount = autoModeState?.runningTasks?.length ?? 0;
|
||||||
|
|
||||||
@@ -139,6 +172,43 @@ export function BottomDock({ className }: BottomDockProps) {
|
|||||||
[activeTab, isExpanded]
|
[activeTab, isExpanded]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get keyboard shortcuts from config
|
||||||
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
|
|
||||||
|
// Register keyboard shortcuts for dock tabs
|
||||||
|
useKeyboardShortcuts([
|
||||||
|
{
|
||||||
|
key: shortcuts.terminal,
|
||||||
|
action: () => handleTabClick('terminal'),
|
||||||
|
description: 'Toggle Terminal panel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: shortcuts.ideation,
|
||||||
|
action: () => handleTabClick('ideation'),
|
||||||
|
description: 'Toggle Ideation panel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: shortcuts.spec,
|
||||||
|
action: () => handleTabClick('spec'),
|
||||||
|
description: 'Toggle Spec panel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: shortcuts.context,
|
||||||
|
action: () => handleTabClick('context'),
|
||||||
|
description: 'Toggle Context panel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: shortcuts.githubIssues,
|
||||||
|
action: () => handleTabClick('github'),
|
||||||
|
description: 'Toggle GitHub panel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: shortcuts.agent,
|
||||||
|
action: () => handleTabClick('agents'),
|
||||||
|
description: 'Toggle Agents panel',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const handleDoubleClick = useCallback(() => {
|
const handleDoubleClick = useCallback(() => {
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
setIsMaximized(!isMaximized);
|
setIsMaximized(!isMaximized);
|
||||||
|
|||||||
@@ -1,18 +1,49 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { CircleDot, GitPullRequest, RefreshCw, ExternalLink, Loader2 } from 'lucide-react';
|
import {
|
||||||
import { getElectronAPI, GitHubIssue, GitHubPR } from '@/lib/electron';
|
CircleDot,
|
||||||
|
GitPullRequest,
|
||||||
|
RefreshCw,
|
||||||
|
ExternalLink,
|
||||||
|
Loader2,
|
||||||
|
Wand2,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
getElectronAPI,
|
||||||
|
GitHubIssue,
|
||||||
|
GitHubPR,
|
||||||
|
IssueValidationResult,
|
||||||
|
StoredValidation,
|
||||||
|
} from '@/lib/electron';
|
||||||
import { useAppStore, GitHubCacheIssue, GitHubCachePR } from '@/store/app-store';
|
import { useAppStore, GitHubCacheIssue, GitHubCachePR } from '@/store/app-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useIssueValidation } from '@/components/views/github-issues-view/hooks';
|
||||||
|
import { ValidationDialog } from '@/components/views/github-issues-view/dialogs';
|
||||||
|
import { useModelOverride } from '@/components/shared';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type GitHubTab = 'issues' | 'prs';
|
type GitHubTab = 'issues' | 'prs';
|
||||||
|
|
||||||
// Cache duration: 5 minutes
|
// Cache duration: 5 minutes
|
||||||
const CACHE_DURATION_MS = 5 * 60 * 1000;
|
const CACHE_DURATION_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
// Check if validation is stale (> 24 hours)
|
||||||
|
function isValidationStale(validatedAt: string): boolean {
|
||||||
|
const VALIDATION_CACHE_TTL_HOURS = 24;
|
||||||
|
const validatedTime = new Date(validatedAt).getTime();
|
||||||
|
const hoursSinceValidation = (Date.now() - validatedTime) / (1000 * 60 * 60);
|
||||||
|
return hoursSinceValidation > VALIDATION_CACHE_TTL_HOURS;
|
||||||
|
}
|
||||||
|
|
||||||
export function GitHubPanel() {
|
export function GitHubPanel() {
|
||||||
const { currentProject, getGitHubCache, setGitHubCache, setGitHubCacheFetching } = useAppStore();
|
const { currentProject, getGitHubCache, setGitHubCache, setGitHubCacheFetching } = useAppStore();
|
||||||
const [activeTab, setActiveTab] = useState<GitHubTab>('issues');
|
const [activeTab, setActiveTab] = useState<GitHubTab>('issues');
|
||||||
|
const [selectedIssue, setSelectedIssue] = useState<GitHubIssue | null>(null);
|
||||||
|
const [validationResult, setValidationResult] = useState<IssueValidationResult | null>(null);
|
||||||
|
const [showValidationDialog, setShowValidationDialog] = useState(false);
|
||||||
const fetchingRef = useRef(false);
|
const fetchingRef = useRef(false);
|
||||||
|
|
||||||
const projectPath = currentProject?.path || '';
|
const projectPath = currentProject?.path || '';
|
||||||
@@ -24,6 +55,18 @@ export function GitHubPanel() {
|
|||||||
const lastFetched = cache?.lastFetched || null;
|
const lastFetched = cache?.lastFetched || null;
|
||||||
const hasCache = issues.length > 0 || prs.length > 0 || lastFetched !== null;
|
const hasCache = issues.length > 0 || prs.length > 0 || lastFetched !== null;
|
||||||
|
|
||||||
|
// Model override for validation
|
||||||
|
const validationModelOverride = useModelOverride({ phase: 'validationModel' });
|
||||||
|
|
||||||
|
// Use the issue validation hook
|
||||||
|
const { validatingIssues, cachedValidations, handleValidateIssue, handleViewCachedValidation } =
|
||||||
|
useIssueValidation({
|
||||||
|
selectedIssue,
|
||||||
|
showValidationDialog,
|
||||||
|
onValidationResultChange: setValidationResult,
|
||||||
|
onShowValidationDialogChange: setShowValidationDialog,
|
||||||
|
});
|
||||||
|
|
||||||
const fetchData = useCallback(
|
const fetchData = useCallback(
|
||||||
async (isBackgroundRefresh = false) => {
|
async (isBackgroundRefresh = false) => {
|
||||||
if (!projectPath || fetchingRef.current) return;
|
if (!projectPath || fetchingRef.current) return;
|
||||||
@@ -123,6 +166,61 @@ export function GitHubPanel() {
|
|||||||
api.openExternalLink(url);
|
api.openExternalLink(url);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle validation for an issue (converts cache issue to GitHubIssue format)
|
||||||
|
const handleValidate = useCallback(
|
||||||
|
(cacheIssue: GitHubCacheIssue) => {
|
||||||
|
// Convert cache issue to GitHubIssue format for validation
|
||||||
|
const issue: GitHubIssue = {
|
||||||
|
number: cacheIssue.number,
|
||||||
|
title: cacheIssue.title,
|
||||||
|
url: cacheIssue.url,
|
||||||
|
author: cacheIssue.author || { login: 'unknown' },
|
||||||
|
state: 'OPEN',
|
||||||
|
body: '',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
labels: [],
|
||||||
|
comments: { totalCount: 0 },
|
||||||
|
};
|
||||||
|
setSelectedIssue(issue);
|
||||||
|
handleValidateIssue(issue, {
|
||||||
|
modelEntry: validationModelOverride.effectiveModelEntry,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleValidateIssue, validationModelOverride.effectiveModelEntry]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle viewing cached validation
|
||||||
|
const handleViewValidation = useCallback(
|
||||||
|
(cacheIssue: GitHubCacheIssue) => {
|
||||||
|
// Convert cache issue to GitHubIssue format
|
||||||
|
const issue: GitHubIssue = {
|
||||||
|
number: cacheIssue.number,
|
||||||
|
title: cacheIssue.title,
|
||||||
|
url: cacheIssue.url,
|
||||||
|
author: cacheIssue.author || { login: 'unknown' },
|
||||||
|
state: 'OPEN',
|
||||||
|
body: '',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
labels: [],
|
||||||
|
comments: { totalCount: 0 },
|
||||||
|
};
|
||||||
|
setSelectedIssue(issue);
|
||||||
|
handleViewCachedValidation(issue);
|
||||||
|
},
|
||||||
|
[handleViewCachedValidation]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get validation status for an issue
|
||||||
|
const getValidationStatus = useCallback(
|
||||||
|
(issueNumber: number) => {
|
||||||
|
const isValidating = validatingIssues.has(issueNumber);
|
||||||
|
const cached = cachedValidations.get(issueNumber);
|
||||||
|
const isStale = cached ? isValidationStale(cached.validatedAt) : false;
|
||||||
|
return { isValidating, cached, isStale };
|
||||||
|
},
|
||||||
|
[validatingIssues, cachedValidations]
|
||||||
|
);
|
||||||
|
|
||||||
// Only show loading spinner if no cached data AND fetching
|
// Only show loading spinner if no cached data AND fetching
|
||||||
if (!hasCache && isFetching) {
|
if (!hasCache && isFetching) {
|
||||||
return (
|
return (
|
||||||
@@ -180,22 +278,82 @@ export function GitHubPanel() {
|
|||||||
issues.length === 0 ? (
|
issues.length === 0 ? (
|
||||||
<p className="text-xs text-muted-foreground text-center py-4">No open issues</p>
|
<p className="text-xs text-muted-foreground text-center py-4">No open issues</p>
|
||||||
) : (
|
) : (
|
||||||
issues.map((issue) => (
|
issues.map((issue) => {
|
||||||
<div
|
const { isValidating, cached, isStale } = getValidationStatus(issue.number);
|
||||||
key={issue.number}
|
|
||||||
className="flex items-start gap-2 p-2 rounded-md hover:bg-accent/50 cursor-pointer group"
|
return (
|
||||||
onClick={() => handleOpenInGitHub(issue.url)}
|
<div
|
||||||
>
|
key={issue.number}
|
||||||
<CircleDot className="h-3.5 w-3.5 mt-0.5 text-green-500 shrink-0" />
|
className="flex items-start gap-2 p-2 rounded-md hover:bg-accent/50 group"
|
||||||
<div className="flex-1 min-w-0">
|
>
|
||||||
<p className="text-xs font-medium truncate">{issue.title}</p>
|
<CircleDot className="h-3.5 w-3.5 mt-0.5 text-green-500 shrink-0" />
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<div className="flex-1 min-w-0">
|
||||||
#{issue.number} opened by {issue.author?.login}
|
<p className="text-xs font-medium truncate">{issue.title}</p>
|
||||||
</p>
|
<p className="text-[10px] text-muted-foreground">
|
||||||
|
#{issue.number} opened by {issue.author?.login}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
{/* Validation status/action */}
|
||||||
|
{isValidating ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
|
||||||
|
) : cached && !isStale ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleViewValidation(issue);
|
||||||
|
}}
|
||||||
|
title="View validation result"
|
||||||
|
>
|
||||||
|
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||||
|
</Button>
|
||||||
|
) : cached && isStale ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleValidate(issue);
|
||||||
|
}}
|
||||||
|
title="Re-validate (stale)"
|
||||||
|
>
|
||||||
|
<Clock className="h-3.5 w-3.5 text-yellow-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleValidate(issue);
|
||||||
|
}}
|
||||||
|
title="Validate with AI"
|
||||||
|
>
|
||||||
|
<Wand2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{/* Open in GitHub */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-1.5 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleOpenInGitHub(issue.url);
|
||||||
|
}}
|
||||||
|
title="Open in GitHub"
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ExternalLink className="h-3 w-3 opacity-0 group-hover:opacity-100 text-muted-foreground" />
|
);
|
||||||
</div>
|
})
|
||||||
))
|
|
||||||
)
|
)
|
||||||
) : prs.length === 0 ? (
|
) : prs.length === 0 ? (
|
||||||
<p className="text-xs text-muted-foreground text-center py-4">No open pull requests</p>
|
<p className="text-xs text-muted-foreground text-center py-4">No open pull requests</p>
|
||||||
@@ -219,6 +377,18 @@ export function GitHubPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Dialog */}
|
||||||
|
<ValidationDialog
|
||||||
|
open={showValidationDialog}
|
||||||
|
onOpenChange={setShowValidationDialog}
|
||||||
|
issue={selectedIssue}
|
||||||
|
validationResult={validationResult}
|
||||||
|
onConvertToTask={() => {
|
||||||
|
// Task conversion not supported in dock panel - need to go to full view
|
||||||
|
toast.info('Open GitHub Issues view for task conversion');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getElectronAPI } from '@/lib/electron';
|
|||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { XmlSyntaxEditor } from '@/components/ui/xml-syntax-editor';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -473,15 +474,12 @@ export function SpecPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 p-2 overflow-hidden">
|
<div className="flex-1 overflow-hidden bg-muted/30 rounded-md m-2">
|
||||||
<Textarea
|
<XmlSyntaxEditor
|
||||||
value={specContent}
|
value={specContent}
|
||||||
onChange={(e) => setSpecContent(e.target.value)}
|
onChange={(value) => setSpecContent(value)}
|
||||||
className={cn(
|
|
||||||
'h-full w-full resize-none font-mono text-xs',
|
|
||||||
'bg-muted/30 border-0 focus-visible:ring-1'
|
|
||||||
)}
|
|
||||||
placeholder="Enter your app specification..."
|
placeholder="Enter your app specification..."
|
||||||
|
className="h-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-reac
|
|||||||
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
import { useResponsiveKanban } from '@/hooks/use-responsive-kanban';
|
||||||
import { getColumnsWithPipeline, type ColumnId } from './constants';
|
import { getColumnsWithPipeline, type ColumnId } from './constants';
|
||||||
import type { PipelineConfig } from '@automaker/types';
|
import type { PipelineConfig } from '@automaker/types';
|
||||||
|
import { useDockState } from '@/components/layout/bottom-dock/bottom-dock';
|
||||||
|
|
||||||
interface KanbanBoardProps {
|
interface KanbanBoardProps {
|
||||||
sensors: any;
|
sensors: any;
|
||||||
@@ -95,8 +96,33 @@ export function KanbanBoard({
|
|||||||
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
|
// containerStyle handles centering and ensures columns fit without horizontal scroll in Electron
|
||||||
const { columnWidth, containerStyle } = useResponsiveKanban(columns.length);
|
const { columnWidth, containerStyle } = useResponsiveKanban(columns.length);
|
||||||
|
|
||||||
|
// Get dock state to add padding when dock is expanded on the side
|
||||||
|
const {
|
||||||
|
position: dockPosition,
|
||||||
|
isExpanded: dockExpanded,
|
||||||
|
isMaximized: dockMaximized,
|
||||||
|
} = useDockState();
|
||||||
|
|
||||||
|
// Calculate padding based on dock state
|
||||||
|
// Dock widths: collapsed=w-10 (2.5rem), expanded=w-96 (24rem), maximized=w-[50vw]
|
||||||
|
const getSideDockPadding = () => {
|
||||||
|
if (!dockExpanded) return undefined;
|
||||||
|
if (dockMaximized) return '50vw';
|
||||||
|
return '25rem'; // 24rem dock width + 1rem breathing room
|
||||||
|
};
|
||||||
|
|
||||||
|
const sideDockPadding = getSideDockPadding();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-x-auto px-5 pb-4 relative" style={backgroundImageStyle}>
|
<div
|
||||||
|
className="flex-1 overflow-x-auto px-5 pb-4 relative transition-[padding] duration-300"
|
||||||
|
style={{
|
||||||
|
...backgroundImageStyle,
|
||||||
|
// Add padding when dock is expanded on the side so content can scroll past the overlay
|
||||||
|
paddingRight: dockPosition === 'right' ? sideDockPadding : undefined,
|
||||||
|
paddingLeft: dockPosition === 'left' ? sideDockPadding : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={collisionDetectionStrategy}
|
collisionDetection={collisionDetectionStrategy}
|
||||||
|
|||||||
Reference in New Issue
Block a user