feat: initial public release (MAESTRO)

This commit is contained in:
oss-sync
2026-06-03 05:08:00 +00:00
commit f5c7666f6b
823 changed files with 184150 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
import { LocalTask } from '../../api';
import { matchText } from '../../lib/utils';
import { COLUMN_LIST, SortMode, StatusColumn } from '../../lib/urlState';
import { FilterBar } from './FilterBar';
import { LocalTaskListItem } from './TaskListItem';
import { RailPanel } from './RailPanel';
interface TaskListPanelProps {
localTasks: LocalTask[];
selectedStatus: 'all' | StatusColumn;
sortMode: SortMode;
searchQuery: string;
activeTaskId: number | null;
onStatusChange: (status: 'all' | StatusColumn) => void;
onSortChange: (sort: SortMode) => void;
onSearchChange: (q: string) => void;
onSelectTask: (id: number) => void;
onOpenCreate: () => void;
/** 'rail' 時は RailPanel を render する。default 'list'。 */
mode?: 'list' | 'rail';
/** rail mode 時の「リストに戻る」ボタンで呼ばれる。 */
onExitFocused?: () => void;
}
export function TaskListPanel({
localTasks,
selectedStatus,
sortMode,
searchQuery,
activeTaskId,
onStatusChange,
onSortChange,
onSearchChange,
onSelectTask,
onOpenCreate,
mode = 'list',
onExitFocused,
}: TaskListPanelProps) {
if (mode === 'rail') {
const localColumnsRail: Record<string, LocalTask[]> = COLUMN_LIST.reduce((acc, s) => {
acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s);
return acc;
}, {} as Record<string, LocalTask[]>);
const allRail = Object.values(localColumnsRail).flat();
const baseRail = selectedStatus === 'all' ? allRail : localColumnsRail[selectedStatus] ?? [];
const filteredRail = baseRail.filter(t =>
matchText(t.title, searchQuery) || matchText(t.body, searchQuery) || matchText(t.pieceName, searchQuery) || matchText(t.ownerName ?? '', searchQuery),
);
return (
<RailPanel
tasks={filteredRail}
activeTaskId={activeTaskId}
onSelectTask={onSelectTask}
onOpenCreate={onOpenCreate}
onExitFocused={onExitFocused ?? (() => {})}
/>
);
}
const localColumns: Record<string, LocalTask[]> = COLUMN_LIST.reduce((acc, s) => {
acc[s] = localTasks.filter(t => (t.latestJob?.status ?? 'queued') === s);
return acc;
}, {} as Record<string, LocalTask[]>);
const allLocalTasks = Object.values(localColumns).flat();
const baseList: LocalTask[] =
selectedStatus === 'all' ? allLocalTasks : localColumns[selectedStatus] ?? [];
const filtered = baseList.filter(t =>
matchText(t.title, searchQuery) || matchText(t.body, searchQuery) || matchText(t.pieceName, searchQuery) || matchText(t.ownerName ?? '', searchQuery)
).sort((a, b) => {
if (sortMode === 'title') {
return a.title.localeCompare(b.title);
}
if (sortMode === 'status') {
const aStatus = a.latestJob?.status ?? 'queued';
const bStatus = b.latestJob?.status ?? 'queued';
return aStatus.localeCompare(bStatus);
}
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
});
const counts: Record<string, number> = {};
for (const s of COLUMN_LIST) {
counts[s] = localColumns[s]?.length ?? 0;
}
const totalCount = allLocalTasks.length;
const runningCount = counts.running ?? 0;
const waitingCount = (counts.waiting_human ?? 0) + (counts.waiting_subtasks ?? 0);
const failedCount = counts.failed ?? 0;
return (
<div className="flex flex-col h-full overflow-hidden">
<button
type="button"
onClick={onOpenCreate}
className="w-full mb-3 px-3 py-2 bg-accent hover:bg-accent-deep active:scale-[0.98] active:bg-accent-deep text-accent-fg rounded-md text-xs font-semibold inline-flex items-center justify-center gap-1.5 transition-[transform,background-color,color] duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring"
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2.25}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 5v14M5 12h14" />
</svg>
</button>
<div className="flex items-center gap-3 text-[10px] text-slate-500 px-0.5 pb-2.5 font-mono tabular-nums">
<span><span className="font-semibold text-slate-700">{totalCount}</span> </span>
<span aria-hidden="true" className="text-slate-300">·</span>
<span><span className="font-semibold text-emerald-600">{runningCount}</span> </span>
<span><span className="font-semibold text-amber-600">{waitingCount}</span> </span>
{failedCount > 0 && (
<span><span className="font-semibold text-red-600">{failedCount}</span> </span>
)}
</div>
<FilterBar
selectedStatus={selectedStatus}
sortMode={sortMode}
searchQuery={searchQuery}
counts={counts}
totalCount={totalCount}
onStatusChange={onStatusChange}
onSortChange={onSortChange}
onSearchChange={onSearchChange}
/>
<div className="flex flex-col gap-1.5 mt-2 overflow-y-auto flex-1 min-h-0 pr-0.5">
{filtered.map(task => (
<LocalTaskListItem
key={task.id}
task={task}
active={activeTaskId === task.id}
onClick={() => onSelectTask(task.id)}
/>
))}
{filtered.length === 0 && (
<div className="text-[13px] text-slate-500 px-2 py-3"></div>
)}
</div>
</div>
);
}