Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/frontend/src/renderer/components/KanbanPilotView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
AutoCodeClientProvider,
BoardSkeleton,
BoardView,
TaskDetail as UiTaskDetail,
buildBoardViewLabels,
Expand Down Expand Up @@ -47,7 +48,7 @@ function PilotBoard({ onOpen }: Readonly<{ onOpen: (id: string) => void }>) {
return (
<div className="h-full overflow-auto p-4">
<h1 className="mb-4 text-lg font-semibold">{t('kanban:pilot.title')}</h1>
{loading && <p>{t('kanban:pilot.loading')}</p>}
{loading && <BoardSkeleton label={t('kanban:pilot.loading')} />}
{error && (
<p role="alert">
{t('kanban:pilot.error')}{' '}
Expand All @@ -62,6 +63,8 @@ function PilotBoard({ onOpen }: Readonly<{ onOpen: (id: string) => void }>) {
columns={columns}
labels={labels}
onSelectTask={(task) => onOpen(task.id)}
emptyTitle={t('kanban:pilot.empty.title')}
emptyDescription={t('kanban:pilot.empty.description')}
/>
)}
</div>
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/shared/i18n/locales/en/kanban.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"loading": "Loading tasks…",
"error": "Failed to load tasks.",
"retry": "Retry",
"empty": {
"title": "No tasks yet",
"description": "Create your first spec to see it on the board."
},
"columns": {
"draft": "Draft",
"running": "Running",
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/shared/i18n/locales/fr/kanban.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"loading": "Chargement des tâches…",
"error": "Échec du chargement des tâches.",
"retry": "Réessayer",
"empty": {
"title": "Aucune tâche pour le moment",
"description": "Créez votre première spéc pour la voir sur le tableau."
},
"columns": {
"draft": "Brouillon",
"running": "En cours",
Expand Down
4 changes: 4 additions & 0 deletions apps/web-frontend/src/i18n/locales/en/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@
"loading": "Loading tasks…",
"error": "Failed to load tasks.",
"retry": "Retry",
"empty": {
"title": "No tasks yet",
"description": "Create your first spec to see it on the board."
},
"columns": {
"draft": "Draft",
"running": "Running",
Expand Down
4 changes: 4 additions & 0 deletions apps/web-frontend/src/i18n/locales/fr/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@
"loading": "Chargement des tâches…",
"error": "Échec du chargement des tâches.",
"retry": "Réessayer",
"empty": {
"title": "Aucune tâche pour le moment",
"description": "Créez votre première spéc pour la voir sur le tableau."
},
"columns": {
"draft": "Brouillon",
"running": "En cours",
Expand Down
5 changes: 4 additions & 1 deletion apps/web-frontend/src/pages/KanbanPilot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import type { KanbanColumn, UiTask } from "@auto-code/ui";
import {
AutoCodeClientProvider,
BoardSkeleton,
BoardView,
buildBoardViewLabels,
useTasks,
Expand Down Expand Up @@ -45,7 +46,7 @@ function PilotBoard() {
return (
<div style={{ padding: "1rem", height: "100%", overflow: "auto" }}>
<h1>{t("tasks:kanbanPilot.title")}</h1>
{loading && <p>{t("tasks:kanbanPilot.loading")}</p>}
{loading && <BoardSkeleton label={t("tasks:kanbanPilot.loading")} />}
{error && (
<p role="alert">
{t("tasks:kanbanPilot.error")}{" "}
Expand All @@ -60,6 +61,8 @@ function PilotBoard() {
columns={columns}
labels={labels}
onSelectTask={handleSelect}
emptyTitle={t("tasks:kanbanPilot.empty.title")}
emptyDescription={t("tasks:kanbanPilot.empty.description")}
/>
)}
</div>
Expand Down
3 changes: 3 additions & 0 deletions libs/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export type {
BoardViewLabels,
Translate,
} from './screens/BoardView';
// Board data states (U4)
export { BoardSkeleton } from './screens/BoardSkeleton';
export type { BoardSkeletonProps } from './screens/BoardSkeleton';
export { filterUiTasks } from './client/filtering';
export type { UiTaskFilter } from './client/filtering';
export { useBoardFilter, FILTER_IDS } from './client/useBoardFilter';
Expand Down
67 changes: 67 additions & 0 deletions libs/ui/src/screens/BoardSkeleton.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.ac-board-skeleton {
display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr));
gap: 14px;
align-items: start;
padding: 18px 28px 28px;
font-family: var(--font-sans);
}

.ac-board-skeleton__column {
min-width: 0;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--soft);
}

.ac-board-skeleton__head {
height: 40px;
border-bottom: 1px solid var(--line);
}

.ac-board-skeleton__body {
display: grid;
gap: 10px;
padding: 10px;
}

.ac-board-skeleton__card {
height: 64px;
border-radius: 10px;
background: linear-gradient(
90deg,
var(--rail) 25%,
var(--line) 37%,
var(--rail) 63%
);
background-size: 400% 100%;
animation: ac-board-skeleton-shimmer 1.4s ease-in-out infinite;
}

@keyframes ac-board-skeleton-shimmer {
from {
background-position: 100% 0;
}
to {
background-position: 0 0;
}
}

@media (prefers-reduced-motion: reduce) {
.ac-board-skeleton__card {
animation: none;
}
}

@media (max-width: 1180px) {
.ac-board-skeleton {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

@media (max-width: 760px) {
.ac-board-skeleton {
grid-template-columns: 1fr;
padding: 14px;
}
}
40 changes: 40 additions & 0 deletions libs/ui/src/screens/BoardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import './BoardSkeleton.css';

export interface BoardSkeletonProps {
/** Number of placeholder columns (defaults to 4, matching the board). */
columns?: number;
/** Accessible loading label (announced to screen readers). */
label?: string;
}

const CARDS_PER_COLUMN = [2, 3, 1, 2];

/**
* Shimmer placeholder shown while the board's tasks load, mirroring the
* KanbanBoard column/card layout so the transition to real data doesn't
* shift the page. Presentational — pair with `useTasks().loading`.
*/
export function BoardSkeleton({ columns = 4, label }: Readonly<BoardSkeletonProps>) {
return (
<div
className="ac-board-skeleton"
role="status"
aria-busy="true"
aria-label={label}
>

Check warning on line 24 in libs/ui/src/screens/BoardSkeleton.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <output> instead of the "status" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ897WafoL1ZW2-k5EoR&open=AZ897WafoL1ZW2-k5EoR&pullRequest=410
{Array.from({ length: columns }, (_, columnIndex) => (
<div key={columnIndex} className="ac-board-skeleton__column">
<div className="ac-board-skeleton__head" />
<div className="ac-board-skeleton__body">
{Array.from(
{ length: CARDS_PER_COLUMN[columnIndex % CARDS_PER_COLUMN.length] },
(_, cardIndex) => (
<div key={cardIndex} className="ac-board-skeleton__card" />
),
)}
</div>
</div>
))}
</div>
);
}
27 changes: 27 additions & 0 deletions libs/ui/src/screens/BoardView.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,30 @@
font-family: var(--font-sans);
font-size: 12.5px;
}

.ac-board-view__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 320px;
padding: 48px 28px;
text-align: center;
font-family: var(--font-sans);
}

.ac-board-view__empty-title {
margin: 0;
color: var(--ink);
font-size: 15px;
font-weight: 760;
}

.ac-board-view__empty-desc {
margin: 0;
max-width: 42ch;
color: var(--muted);
font-size: 13px;
line-height: 1.5;
}
20 changes: 20 additions & 0 deletions libs/ui/src/screens/BoardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,23 +62,43 @@ export interface BoardViewProps {
columns?: KanbanColumn[];
labels: BoardViewLabels;
onSelectTask: (task: UiTask) => void;
/** Heading for the genuinely-empty board (no tasks at all). */
emptyTitle?: string;
/** Sub-text under the empty heading. */
emptyDescription?: string;
}

/**
* The filterable Kanban board: BoardToolbar (search + filter chips + view
* switch) over a live-narrowing KanbanBoard, with a polite status line for
* the match count (WCAG 4.1.3). Both app pilots render this identically;
* loading/error states stay with the caller.
*
* With zero tasks it shows a centered empty state (when emptyTitle is given)
* instead of an empty toolbar over four blank columns.
*/
export function BoardView({
tasks,
columns,
labels,
onSelectTask,
emptyTitle,
emptyDescription,
}: Readonly<BoardViewProps>) {
const { query, setQuery, filterId, setFilterId, filtering, visibleTasks } =
useBoardFilter(tasks);

if (tasks.length === 0 && emptyTitle != null) {
return (
<div className="ac-board-view__empty">
<h2 className="ac-board-view__empty-title">{emptyTitle}</h2>
{emptyDescription != null && (
<p className="ac-board-view__empty-desc">{emptyDescription}</p>
)}
</div>
);
}

let matchStatus = '';
if (filtering) {
matchStatus =
Expand Down
Loading