diff --git a/apps/frontend/src/renderer/components/KanbanBoard.tsx b/apps/frontend/src/renderer/components/KanbanBoard.tsx
index 7522ac2a..53d92b10 100644
--- a/apps/frontend/src/renderer/components/KanbanBoard.tsx
+++ b/apps/frontend/src/renderer/components/KanbanBoard.tsx
@@ -19,7 +19,7 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy
} from '@dnd-kit/sortable';
-import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw, GitPullRequest, X, Settings, ListPlus } from 'lucide-react';
+import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive, RefreshCw, GitPullRequest, X, Settings, ListPlus, ChevronLeft, ChevronRight, ChevronsRight, Lock, Unlock } from 'lucide-react';
import { Checkbox } from './ui/checkbox';
import { ScrollArea } from './ui/scroll-area';
import { Button } from './ui/button';
@@ -31,6 +31,7 @@ import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants'
import { cn } from '../lib/utils';
import { persistTaskStatus, forceCompleteTask, archiveTasks, useTaskStore } from '../stores/task-store';
import { updateProjectSettings, useProjectStore } from '../stores/project-store';
+import { useKanbanSettingsStore, COLLAPSED_COLUMN_WIDTH, DEFAULT_COLUMN_WIDTH, MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH } from '../stores/kanban-settings-store';
import { useToast } from '../hooks/use-toast';
import { WorktreeCleanupDialog } from './WorktreeCleanupDialog';
import { BulkPRDialog } from './BulkPRDialog';
@@ -81,6 +82,17 @@ interface DroppableColumnProps {
onSelectAll?: () => void;
onDeselectAll?: () => void;
onToggleSelect?: (taskId: string) => void;
+ // Collapse props
+ isCollapsed?: boolean;
+ onToggleCollapsed?: () => void;
+ // Resize props
+ columnWidth?: number;
+ isResizing?: boolean;
+ onResizeStart?: (startX: number) => void;
+ onResizeEnd?: () => void;
+ // Lock props
+ isLocked?: boolean;
+ onToggleLocked?: () => void;
}
/**
@@ -130,6 +142,14 @@ function droppableColumnPropsAreEqual(
if (prevProps.onSelectAll !== nextProps.onSelectAll) return false;
if (prevProps.onDeselectAll !== nextProps.onDeselectAll) return false;
if (prevProps.onToggleSelect !== nextProps.onToggleSelect) return false;
+ if (prevProps.isCollapsed !== nextProps.isCollapsed) return false;
+ if (prevProps.onToggleCollapsed !== nextProps.onToggleCollapsed) return false;
+ if (prevProps.columnWidth !== nextProps.columnWidth) return false;
+ if (prevProps.isResizing !== nextProps.isResizing) return false;
+ if (prevProps.onResizeStart !== nextProps.onResizeStart) return false;
+ if (prevProps.onResizeEnd !== nextProps.onResizeEnd) return false;
+ if (prevProps.isLocked !== nextProps.isLocked) return false;
+ if (prevProps.onToggleLocked !== nextProps.onToggleLocked) return false;
// Compare selection props
const prevSelected = prevProps.selectedTaskIds;
@@ -200,7 +220,7 @@ const getEmptyStateContent = (status: TaskStatus, t: (key: string) => string): {
}
};
-const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskClick, onStatusChange, isOver, onAddClick, onArchiveAll, onQueueSettings, onQueueAll, maxParallelTasks, archivedCount, showArchived, onToggleArchived, selectedTaskIds, onSelectAll, onDeselectAll, onToggleSelect }: DroppableColumnProps) {
+const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskClick, onStatusChange, isOver, onAddClick, onArchiveAll, onQueueSettings, onQueueAll, maxParallelTasks, archivedCount, showArchived, onToggleArchived, selectedTaskIds, onSelectAll, onDeselectAll, onToggleSelect, isCollapsed, onToggleCollapsed, columnWidth, isResizing, onResizeStart, onResizeEnd, isLocked, onToggleLocked }: DroppableColumnProps) {
const { t } = useTranslation(['tasks', 'common']);
const { setNodeRef } = useDroppable({
id: status
@@ -298,19 +318,94 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
const emptyState = getEmptyStateContent(status, t);
+ // Collapsed state: show narrow vertical strip with rotated title and task count
+ if (isCollapsed) {
+ return (
+
+ {/* Expand button at top */}
+
+
+
+
+
+
+ {t('kanban.expandColumn')}
+
+
+
+
+ {/* Rotated title and task count */}
+
+
+
+ {tasks.length}
+
+
+ {t(TASK_STATUS_LABELS[status])}
+
+
+
+
+ );
+ }
+
return (
- {/* Column header - enhanced styling */}
-
+
+ {/* Column header - enhanced styling */}
+
+ {/* Collapse button */}
+ {onToggleCollapsed && (
+
+
+
+
+
+ {t('kanban.collapseColumn')}
+
+
+ )}
{/* Select All checkbox for human_review column */}
{isHumanReview && onSelectAll && onDeselectAll && (
@@ -347,6 +442,31 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
)}
+ {/* Lock toggle button - available for all columns */}
+ {onToggleLocked && (
+
+
+
+
+
+ {isLocked ? t('kanban.unlockColumn') : t('kanban.lockColumn')}
+
+
+ )}
{status === 'backlog' && (
<>
{onQueueAll && tasks.length > 0 && (
@@ -468,6 +588,39 @@ const DroppableColumn = memo(function DroppableColumn({ status, tasks, onTaskCli
+
+
+ {/* Resize handle on right edge */}
+ {onResizeStart && onResizeEnd && (
+
{
+ e.preventDefault();
+ // Don't start resize if column is locked
+ if (isLocked) return;
+ onResizeStart(e.clientX);
+ }}
+ onTouchStart={(e) => {
+ e.preventDefault();
+ // Don't start resize if column is locked
+ if (isLocked) return;
+ if (e.touches.length > 0) {
+ onResizeStart(e.touches[0].clientX);
+ }
+ }}
+ title={isLocked ? t('kanban.columnLocked') : undefined}
+ >
+ {/* Wider invisible hit area for easier grabbing */}
+
+
+ )}
);
}, droppableColumnPropsAreEqual);
@@ -482,6 +635,22 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
// Project store for queue settings
const projects = useProjectStore((state) => state.projects);
+ // Kanban settings store for column preferences (collapse state, width, lock state)
+ const columnPreferences = useKanbanSettingsStore((state) => state.columnPreferences);
+ const loadKanbanPreferences = useKanbanSettingsStore((state) => state.loadPreferences);
+ const saveKanbanPreferences = useKanbanSettingsStore((state) => state.savePreferences);
+ const toggleColumnCollapsed = useKanbanSettingsStore((state) => state.toggleColumnCollapsed);
+ const setColumnCollapsed = useKanbanSettingsStore((state) => state.setColumnCollapsed);
+ const setColumnWidth = useKanbanSettingsStore((state) => state.setColumnWidth);
+ const toggleColumnLocked = useKanbanSettingsStore((state) => state.toggleColumnLocked);
+
+ // Column resize state
+ const [resizingColumn, setResizingColumn] = useState
(null);
+ const resizeStartX = useRef(0);
+ const resizeStartWidth = useRef(0);
+ // Capture projectId at resize start to avoid stale closure if project changes during resize
+ const resizeProjectIdRef = useRef(null);
+
// Get projectId from first task
const projectId = tasks[0]?.projectId;
const project = projectId ? projects.find((p) => p.id === projectId) : undefined;
@@ -524,6 +693,14 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
[tasks]
);
+ // Calculate collapsed column count for "Expand All" button
+ const collapsedColumnCount = useMemo(() => {
+ if (!columnPreferences) return 0;
+ return TASK_STATUS_COLUMNS.filter(
+ (status) => columnPreferences[status]?.isCollapsed
+ ).length;
+ }, [columnPreferences]);
+
// Filter tasks based on archive status
const filteredTasks = useMemo(() => {
if (showArchived) {
@@ -932,6 +1109,127 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
}
}, [projectId, loadTaskOrder]);
+ // Load kanban column preferences on mount and when project changes
+ useEffect(() => {
+ if (projectId) {
+ loadKanbanPreferences(projectId);
+ }
+ }, [projectId, loadKanbanPreferences]);
+
+ // Create a callback to toggle collapsed state and save to storage
+ const handleToggleColumnCollapsed = useCallback((status: typeof TASK_STATUS_COLUMNS[number]) => {
+ // Capture projectId at function start to avoid stale closure in setTimeout
+ const currentProjectId = projectId;
+ toggleColumnCollapsed(status);
+ // Save preferences after toggling
+ if (currentProjectId) {
+ // Use setTimeout to ensure state is updated before saving
+ setTimeout(() => {
+ saveKanbanPreferences(currentProjectId);
+ }, 0);
+ }
+ }, [toggleColumnCollapsed, saveKanbanPreferences, projectId]);
+
+ // Create a callback to expand all collapsed columns and save to storage
+ const handleExpandAll = useCallback(() => {
+ // Capture projectId at function start to avoid stale closure in setTimeout
+ const currentProjectId = projectId;
+ // Expand all collapsed columns
+ for (const status of TASK_STATUS_COLUMNS) {
+ if (columnPreferences?.[status]?.isCollapsed) {
+ setColumnCollapsed(status, false);
+ }
+ }
+ // Save preferences after expanding
+ if (currentProjectId) {
+ setTimeout(() => {
+ saveKanbanPreferences(currentProjectId);
+ }, 0);
+ }
+ }, [columnPreferences, setColumnCollapsed, saveKanbanPreferences, projectId]);
+
+ // Create a callback to toggle locked state and save to storage
+ const handleToggleColumnLocked = useCallback((status: typeof TASK_STATUS_COLUMNS[number]) => {
+ // Capture projectId at function start to avoid stale closure in setTimeout
+ const currentProjectId = projectId;
+ toggleColumnLocked(status);
+ // Save preferences after toggling
+ if (currentProjectId) {
+ // Use setTimeout to ensure state is updated before saving
+ setTimeout(() => {
+ saveKanbanPreferences(currentProjectId);
+ }, 0);
+ }
+ }, [toggleColumnLocked, saveKanbanPreferences, projectId]);
+
+ // Resize handlers for column width adjustment
+ const handleResizeStart = useCallback((status: typeof TASK_STATUS_COLUMNS[number], startX: number) => {
+ const currentWidth = columnPreferences?.[status]?.width ?? DEFAULT_COLUMN_WIDTH;
+ resizeStartX.current = startX;
+ resizeStartWidth.current = currentWidth;
+ // Capture projectId at resize start to ensure we save to the correct project
+ resizeProjectIdRef.current = projectId ?? null;
+ setResizingColumn(status);
+ }, [columnPreferences, projectId]);
+
+ const handleResizeMove = useCallback((clientX: number) => {
+ if (!resizingColumn) return;
+
+ const deltaX = clientX - resizeStartX.current;
+ const newWidth = Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, resizeStartWidth.current + deltaX));
+ setColumnWidth(resizingColumn, newWidth);
+ }, [resizingColumn, setColumnWidth]);
+
+ const handleResizeEnd = useCallback(() => {
+ // Use the projectId captured at resize start to avoid saving to wrong project
+ const savedProjectId = resizeProjectIdRef.current;
+ if (resizingColumn && savedProjectId) {
+ saveKanbanPreferences(savedProjectId);
+ }
+ setResizingColumn(null);
+ resizeProjectIdRef.current = null;
+ }, [resizingColumn, saveKanbanPreferences]);
+
+ // Document-level event listeners for resize dragging
+ useEffect(() => {
+ if (!resizingColumn) return;
+
+ const handleMouseMove = (e: MouseEvent) => {
+ handleResizeMove(e.clientX);
+ };
+
+ const handleMouseUp = () => {
+ handleResizeEnd();
+ };
+
+ const handleTouchMove = (e: TouchEvent) => {
+ if (e.touches.length === 0) return;
+ handleResizeMove(e.touches[0].clientX);
+ };
+
+ const handleTouchEnd = () => {
+ handleResizeEnd();
+ };
+
+ // Prevent text selection and set resize cursor during drag
+ document.body.style.userSelect = 'none';
+ document.body.style.cursor = 'col-resize';
+
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
+ document.addEventListener('touchend', handleTouchEnd);
+
+ return () => {
+ document.body.style.userSelect = '';
+ document.body.style.cursor = '';
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.removeEventListener('touchmove', handleTouchMove);
+ document.removeEventListener('touchend', handleTouchEnd);
+ };
+ }, [resizingColumn, handleResizeMove, handleResizeEnd]);
+
// Clean up stale task IDs from order when tasks change (e.g., after deletion)
// This ensures the persisted order doesn't contain IDs for deleted tasks
useEffect(() => {
@@ -1084,19 +1382,37 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
return (
- {/* Kanban header with refresh button */}
- {onRefresh && (
-
-
+ {/* Kanban header with refresh button and expand all */}
+ {(onRefresh || collapsedColumnCount >= 3) && (
+
+
+ {/* Expand All button - appears when 3+ columns are collapsed */}
+ {collapsedColumnCount >= 3 && (
+
+ )}
+
+
+ {onRefresh && (
+
+ )}
+
)}
{/* Kanban columns */}
@@ -1133,6 +1449,14 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick, onRefresh, isR
onSelectAll={status === 'human_review' ? selectAllTasks : undefined}
onDeselectAll={status === 'human_review' ? deselectAllTasks : undefined}
onToggleSelect={status === 'human_review' ? toggleTaskSelection : undefined}
+ isCollapsed={columnPreferences?.[status]?.isCollapsed}
+ onToggleCollapsed={() => handleToggleColumnCollapsed(status)}
+ columnWidth={columnPreferences?.[status]?.width}
+ isResizing={resizingColumn === status}
+ onResizeStart={(startX) => handleResizeStart(status, startX)}
+ onResizeEnd={handleResizeEnd}
+ isLocked={columnPreferences?.[status]?.isLocked}
+ onToggleLocked={() => handleToggleColumnLocked(status)}
/>
))}
diff --git a/apps/frontend/src/renderer/stores/kanban-settings-store.ts b/apps/frontend/src/renderer/stores/kanban-settings-store.ts
new file mode 100644
index 00000000..35369072
--- /dev/null
+++ b/apps/frontend/src/renderer/stores/kanban-settings-store.ts
@@ -0,0 +1,312 @@
+import { create } from 'zustand';
+import type { TaskStatusColumn } from '../../shared/constants/task';
+import { TASK_STATUS_COLUMNS } from '../../shared/constants/task';
+
+// ============================================
+// Types
+// ============================================
+
+/**
+ * Column preferences for a single kanban column
+ */
+export interface ColumnPreferences {
+ /** Column width in pixels (180-600px range) */
+ width: number;
+ /** Whether the column is collapsed (narrow vertical strip) */
+ isCollapsed: boolean;
+ /** Whether the column width is locked (prevents resize) */
+ isLocked: boolean;
+}
+
+/**
+ * All column preferences keyed by status column
+ */
+export type KanbanColumnPreferences = Record
;
+
+/**
+ * Kanban settings store state
+ */
+interface KanbanSettingsState {
+ /** Column preferences for each status column */
+ columnPreferences: KanbanColumnPreferences | null;
+
+ // Actions
+ /** Initialize column preferences (call on mount) */
+ initializePreferences: () => void;
+ /** Set column width */
+ setColumnWidth: (column: TaskStatusColumn, width: number) => void;
+ /** Toggle column collapsed state */
+ toggleColumnCollapsed: (column: TaskStatusColumn) => void;
+ /** Set column collapsed state explicitly */
+ setColumnCollapsed: (column: TaskStatusColumn, isCollapsed: boolean) => void;
+ /** Toggle column locked state */
+ toggleColumnLocked: (column: TaskStatusColumn) => void;
+ /** Set column locked state explicitly */
+ setColumnLocked: (column: TaskStatusColumn, isLocked: boolean) => void;
+ /** Load preferences from localStorage */
+ loadPreferences: (projectId: string) => void;
+ /** Save preferences to localStorage */
+ savePreferences: (projectId: string) => boolean;
+ /** Reset preferences to defaults */
+ resetPreferences: (projectId: string) => void;
+ /** Get preferences for a single column */
+ getColumnPreferences: (column: TaskStatusColumn) => ColumnPreferences;
+}
+
+// ============================================
+// Constants
+// ============================================
+
+/** localStorage key prefix for kanban settings persistence */
+const KANBAN_SETTINGS_KEY_PREFIX = 'kanban-column-prefs';
+
+/** Default column width in pixels */
+export const DEFAULT_COLUMN_WIDTH = 320;
+
+/** Minimum column width in pixels */
+export const MIN_COLUMN_WIDTH = 180;
+
+/** Maximum column width in pixels */
+export const MAX_COLUMN_WIDTH = 600;
+
+/** Collapsed column width in pixels */
+export const COLLAPSED_COLUMN_WIDTH = 48;
+
+// ============================================
+// Helper Functions
+// ============================================
+
+/**
+ * Get the localStorage key for a project's kanban settings
+ */
+function getKanbanSettingsKey(projectId: string): string {
+ return `${KANBAN_SETTINGS_KEY_PREFIX}-${projectId}`;
+}
+
+/**
+ * Create default column preferences for all columns
+ */
+function createDefaultPreferences(): KanbanColumnPreferences {
+ const preferences: Partial = {};
+
+ for (const column of TASK_STATUS_COLUMNS) {
+ preferences[column] = {
+ width: DEFAULT_COLUMN_WIDTH,
+ isCollapsed: false,
+ isLocked: false
+ };
+ }
+
+ return preferences as KanbanColumnPreferences;
+}
+
+/**
+ * Validate column preferences structure
+ * Returns true if valid, false if invalid/incomplete
+ */
+function validatePreferences(data: unknown): data is KanbanColumnPreferences {
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
+ return false;
+ }
+
+ const prefs = data as Record;
+
+ // Validate each required column exists with correct structure
+ for (const column of TASK_STATUS_COLUMNS) {
+ const columnPrefs = prefs[column];
+
+ if (!columnPrefs || typeof columnPrefs !== 'object') {
+ return false;
+ }
+
+ const cp = columnPrefs as Record;
+
+ // Validate width is a number within bounds
+ if (typeof cp.width !== 'number' || cp.width < MIN_COLUMN_WIDTH || cp.width > MAX_COLUMN_WIDTH) {
+ return false;
+ }
+
+ // Validate boolean fields
+ if (typeof cp.isCollapsed !== 'boolean' || typeof cp.isLocked !== 'boolean') {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Clamp a width value to valid bounds
+ */
+function clampWidth(width: number): number {
+ return Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, width));
+}
+
+// ============================================
+// Store
+// ============================================
+
+export const useKanbanSettingsStore = create((set, get) => ({
+ columnPreferences: null,
+
+ initializePreferences: () => {
+ const state = get();
+ if (!state.columnPreferences) {
+ set({ columnPreferences: createDefaultPreferences() });
+ }
+ },
+
+ setColumnWidth: (column, width) => {
+ set((state) => {
+ if (!state.columnPreferences) return state;
+
+ // Don't allow width changes on locked columns
+ if (state.columnPreferences[column].isLocked) {
+ return state;
+ }
+
+ const clampedWidth = clampWidth(width);
+
+ return {
+ columnPreferences: {
+ ...state.columnPreferences,
+ [column]: {
+ ...state.columnPreferences[column],
+ width: clampedWidth
+ }
+ }
+ };
+ });
+ },
+
+ toggleColumnCollapsed: (column) => {
+ set((state) => {
+ if (!state.columnPreferences) return state;
+
+ return {
+ columnPreferences: {
+ ...state.columnPreferences,
+ [column]: {
+ ...state.columnPreferences[column],
+ isCollapsed: !state.columnPreferences[column].isCollapsed
+ }
+ }
+ };
+ });
+ },
+
+ setColumnCollapsed: (column, isCollapsed) => {
+ set((state) => {
+ if (!state.columnPreferences) return state;
+
+ return {
+ columnPreferences: {
+ ...state.columnPreferences,
+ [column]: {
+ ...state.columnPreferences[column],
+ isCollapsed
+ }
+ }
+ };
+ });
+ },
+
+ toggleColumnLocked: (column) => {
+ set((state) => {
+ if (!state.columnPreferences) return state;
+
+ return {
+ columnPreferences: {
+ ...state.columnPreferences,
+ [column]: {
+ ...state.columnPreferences[column],
+ isLocked: !state.columnPreferences[column].isLocked
+ }
+ }
+ };
+ });
+ },
+
+ setColumnLocked: (column, isLocked) => {
+ set((state) => {
+ if (!state.columnPreferences) return state;
+
+ return {
+ columnPreferences: {
+ ...state.columnPreferences,
+ [column]: {
+ ...state.columnPreferences[column],
+ isLocked
+ }
+ }
+ };
+ });
+ },
+
+ loadPreferences: (projectId) => {
+ try {
+ const key = getKanbanSettingsKey(projectId);
+ const stored = localStorage.getItem(key);
+
+ if (stored) {
+ const parsed = JSON.parse(stored);
+
+ // Validate structure before using
+ if (validatePreferences(parsed)) {
+ set({ columnPreferences: parsed });
+ return;
+ }
+
+ // Invalid data structure, use defaults
+ console.warn('[KanbanSettingsStore] Invalid preferences in localStorage, using defaults');
+ }
+
+ // No stored preferences or invalid, use defaults
+ set({ columnPreferences: createDefaultPreferences() });
+ } catch (error) {
+ console.error('[KanbanSettingsStore] Failed to load preferences:', error);
+ set({ columnPreferences: createDefaultPreferences() });
+ }
+ },
+
+ savePreferences: (projectId) => {
+ try {
+ const state = get();
+ if (!state.columnPreferences) {
+ return false;
+ }
+
+ const key = getKanbanSettingsKey(projectId);
+ localStorage.setItem(key, JSON.stringify(state.columnPreferences));
+ return true;
+ } catch (error) {
+ console.error('[KanbanSettingsStore] Failed to save preferences:', error);
+ return false;
+ }
+ },
+
+ resetPreferences: (projectId) => {
+ try {
+ const key = getKanbanSettingsKey(projectId);
+ localStorage.removeItem(key);
+ set({ columnPreferences: createDefaultPreferences() });
+ } catch (error) {
+ console.error('[KanbanSettingsStore] Failed to reset preferences:', error);
+ }
+ },
+
+ getColumnPreferences: (column) => {
+ const state = get();
+
+ if (!state.columnPreferences) {
+ return {
+ width: DEFAULT_COLUMN_WIDTH,
+ isCollapsed: false,
+ isLocked: false
+ };
+ }
+
+ return state.columnPreferences[column];
+ }
+}));
+
diff --git a/apps/frontend/src/renderer/styles/globals.css b/apps/frontend/src/renderer/styles/globals.css
index dce2d1f3..3164caaf 100644
--- a/apps/frontend/src/renderer/styles/globals.css
+++ b/apps/frontend/src/renderer/styles/globals.css
@@ -1712,3 +1712,4 @@ body {
[data-ui-scale="190"] { font-size: 30.4px; }
[data-ui-scale="195"] { font-size: 31.2px; }
[data-ui-scale="200"] { font-size: 32px; }
+
diff --git a/apps/frontend/src/shared/i18n/locales/en/tasks.json b/apps/frontend/src/shared/i18n/locales/en/tasks.json
index 3f699e3b..f61d4f8f 100644
--- a/apps/frontend/src/shared/i18n/locales/en/tasks.json
+++ b/apps/frontend/src/shared/i18n/locales/en/tasks.json
@@ -100,7 +100,14 @@
"selectedCountOne": "{{count}} task selected",
"selectedCountOther": "{{count}} tasks selected",
"createPRs": "Create PRs",
- "clearSelection": "Clear Selection"
+ "clearSelection": "Clear Selection",
+ "collapseColumn": "Collapse column",
+ "expandColumn": "Expand column",
+ "resizeColumn": "Resize column",
+ "lockColumn": "Lock column width",
+ "unlockColumn": "Unlock column width",
+ "columnLocked": "Column width is locked",
+ "expandAll": "Expand all columns"
},
"queue": {
"limitReached": "Parallel task limit reached ({{current}}/{{max}}). Task moved to queue.",
diff --git a/apps/frontend/src/shared/i18n/locales/fr/tasks.json b/apps/frontend/src/shared/i18n/locales/fr/tasks.json
index 62ec5d26..1acd12c5 100644
--- a/apps/frontend/src/shared/i18n/locales/fr/tasks.json
+++ b/apps/frontend/src/shared/i18n/locales/fr/tasks.json
@@ -99,7 +99,14 @@
"selectedCountOne": "{{count}} tâche sélectionnée",
"selectedCountOther": "{{count}} tâches sélectionnées",
"createPRs": "Créer les PRs",
- "clearSelection": "Effacer la sélection"
+ "clearSelection": "Effacer la sélection",
+ "collapseColumn": "Réduire la colonne",
+ "expandColumn": "Développer la colonne",
+ "resizeColumn": "Redimensionner la colonne",
+ "lockColumn": "Verrouiller la largeur de la colonne",
+ "unlockColumn": "Déverrouiller la largeur de la colonne",
+ "columnLocked": "La largeur de la colonne est verrouillée",
+ "expandAll": "Développer toutes les colonnes"
},
"queue": {
"limitReached": "Limite de tâches parallèles atteinte ({{current}}/{{max}}). Tâche déplacée vers la file d'attente.",