@industry-theme/backlogmd-kanban-panel 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -526,10 +526,32 @@ function extractIdFromPath(filePath) {
526
526
  }
527
527
  return filename.replace(/\.md$/, "");
528
528
  }
529
+ function extractTitleFromPath(filePath) {
530
+ const filename = filePath.split("/").pop() || "";
531
+ const match = filename.match(/^(?:task-)?\d+(?:\.\d+)?\s*-\s*(.+)\.md$/);
532
+ if (match) {
533
+ return match[1].trim();
534
+ }
535
+ return filename.replace(/\.md$/, "");
536
+ }
537
+ function extractSourceFromPath(filePath) {
538
+ if (filePath.includes("/completed/") || filePath.includes("\\completed\\")) {
539
+ return "completed";
540
+ }
541
+ return "tasks";
542
+ }
543
+ function extractTaskIndexFromPath(filePath) {
544
+ return {
545
+ id: extractIdFromPath(filePath),
546
+ filePath,
547
+ title: extractTitleFromPath(filePath),
548
+ source: extractSourceFromPath(filePath)
549
+ };
550
+ }
529
551
  function escapeRegex(str) {
530
552
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
531
553
  }
532
- const DEFAULT_STATUSES$1 = ["To Do", "In Progress", "Done"];
554
+ const DEFAULT_STATUSES = ["To Do", "In Progress", "Done"];
533
555
  function parseBacklogConfig(content) {
534
556
  const config = {};
535
557
  const lines = content.split("\n");
@@ -609,10 +631,10 @@ function parseBacklogConfig(content) {
609
631
  }
610
632
  return {
611
633
  projectName: config.projectName || "Backlog",
612
- statuses: config.statuses || [...DEFAULT_STATUSES$1],
634
+ statuses: config.statuses || [...DEFAULT_STATUSES],
613
635
  labels: config.labels || [],
614
636
  milestones: config.milestones || [],
615
- defaultStatus: config.defaultStatus || DEFAULT_STATUSES$1[0],
637
+ defaultStatus: config.defaultStatus || DEFAULT_STATUSES[0],
616
638
  dateFormat: config.dateFormat || "YYYY-MM-DD",
617
639
  defaultAssignee: config.defaultAssignee,
618
640
  defaultReporter: config.defaultReporter,
@@ -684,6 +706,28 @@ const PRIORITY_ORDER = {
684
706
  medium: 1,
685
707
  low: 2
686
708
  };
709
+ function sortTasksByTitle(tasks, direction = "asc") {
710
+ return [...tasks].sort((a, b) => {
711
+ const cmp = a.title.localeCompare(b.title);
712
+ return direction === "asc" ? cmp : -cmp;
713
+ });
714
+ }
715
+ function sortTasksBy(tasks, sortBy = "title", direction = "asc") {
716
+ switch (sortBy) {
717
+ case "title":
718
+ return sortTasksByTitle(tasks, direction);
719
+ case "createdDate":
720
+ return [...tasks].sort((a, b) => {
721
+ const cmp = a.createdDate.localeCompare(b.createdDate);
722
+ return direction === "asc" ? cmp : -cmp;
723
+ });
724
+ case "priority":
725
+ case "ordinal":
726
+ default:
727
+ const sorted = sortTasks(tasks);
728
+ return direction === "desc" ? sorted.reverse() : sorted;
729
+ }
730
+ }
687
731
  function sortTasks(tasks) {
688
732
  return [...tasks].sort((a, b) => {
689
733
  if (a.ordinal !== void 0 && b.ordinal !== void 0) {
@@ -725,6 +769,9 @@ class Core {
725
769
  __publicField(this, "config", null);
726
770
  __publicField(this, "tasks", /* @__PURE__ */ new Map());
727
771
  __publicField(this, "initialized", false);
772
+ /** Lightweight task index for lazy loading (no file reads) */
773
+ __publicField(this, "taskIndex", /* @__PURE__ */ new Map());
774
+ __publicField(this, "lazyInitialized", false);
728
775
  this.projectRoot = options.projectRoot;
729
776
  this.fs = options.adapters.fs;
730
777
  }
@@ -806,6 +853,173 @@ class Core {
806
853
  }
807
854
  this.initialized = true;
808
855
  }
856
+ /**
857
+ * Initialize with lazy loading (no file content reads)
858
+ *
859
+ * Only loads config and builds task index from file paths.
860
+ * Task content is loaded on-demand via loadTask().
861
+ * Use this for web/panel contexts where file reads are expensive.
862
+ *
863
+ * @param filePaths - Array of all file paths in the project
864
+ */
865
+ async initializeLazy(filePaths) {
866
+ if (this.lazyInitialized)
867
+ return;
868
+ const configPath = this.fs.join(this.projectRoot, "backlog", "config.yml");
869
+ const configExists = await this.fs.exists(configPath);
870
+ if (!configExists) {
871
+ throw new Error(`Not a Backlog.md project: config.yml not found at ${configPath}`);
872
+ }
873
+ const configContent = await this.fs.readFile(configPath);
874
+ this.config = parseBacklogConfig(configContent);
875
+ this.taskIndex.clear();
876
+ for (const filePath of filePaths) {
877
+ if (!filePath.endsWith(".md"))
878
+ continue;
879
+ const isTaskFile = filePath.includes("backlog/tasks/") || filePath.includes("backlog\\tasks\\");
880
+ const isCompletedFile = filePath.includes("backlog/completed/") || filePath.includes("backlog\\completed\\");
881
+ if (!isTaskFile && !isCompletedFile)
882
+ continue;
883
+ if (filePath.endsWith("config.yml"))
884
+ continue;
885
+ const indexEntry = extractTaskIndexFromPath(filePath);
886
+ this.taskIndex.set(indexEntry.id, indexEntry);
887
+ }
888
+ this.lazyInitialized = true;
889
+ }
890
+ /**
891
+ * Check if lazy initialization is complete
892
+ */
893
+ isLazyInitialized() {
894
+ return this.lazyInitialized;
895
+ }
896
+ /**
897
+ * Get the task index (lightweight entries)
898
+ */
899
+ getTaskIndex() {
900
+ if (!this.lazyInitialized) {
901
+ throw new Error("Core not lazy initialized. Call initializeLazy() first.");
902
+ }
903
+ return this.taskIndex;
904
+ }
905
+ /**
906
+ * Load a single task by ID (on-demand loading)
907
+ *
908
+ * @param id - Task ID
909
+ * @returns Task or undefined if not found
910
+ */
911
+ async loadTask(id) {
912
+ if (this.tasks.has(id)) {
913
+ return this.tasks.get(id);
914
+ }
915
+ const indexEntry = this.taskIndex.get(id);
916
+ if (!indexEntry) {
917
+ return void 0;
918
+ }
919
+ try {
920
+ const content = await this.fs.readFile(indexEntry.filePath);
921
+ const task = parseTaskMarkdown(content, indexEntry.filePath);
922
+ task.source = indexEntry.source === "completed" ? "completed" : "local";
923
+ this.tasks.set(task.id, task);
924
+ return task;
925
+ } catch (error) {
926
+ console.warn(`Failed to load task ${id} from ${indexEntry.filePath}:`, error);
927
+ return void 0;
928
+ }
929
+ }
930
+ /**
931
+ * Load multiple tasks by ID (on-demand loading)
932
+ *
933
+ * @param ids - Array of task IDs to load
934
+ * @returns Array of loaded tasks (undefined entries filtered out)
935
+ */
936
+ async loadTasks(ids) {
937
+ const tasks = await Promise.all(ids.map((id) => this.loadTask(id)));
938
+ return tasks.filter((t) => t !== void 0);
939
+ }
940
+ /**
941
+ * Get tasks by source (tasks/completed) with pagination
942
+ *
943
+ * This is a lazy-loading alternative to getTasksByStatusPaginated().
944
+ * Groups by directory (tasks/ or completed/) instead of status.
945
+ * Only loads task content for items in the requested page.
946
+ *
947
+ * @param pagination - Pagination options (applied per source)
948
+ * @returns Paginated tasks grouped by source
949
+ */
950
+ async getTasksBySourcePaginated(pagination) {
951
+ if (!this.lazyInitialized) {
952
+ throw new Error("Core not lazy initialized. Call initializeLazy() first.");
953
+ }
954
+ const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
955
+ const offset = (pagination == null ? void 0 : pagination.offset) ?? 0;
956
+ const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
957
+ const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
958
+ const sources = ["tasks", "completed"];
959
+ const bySource = /* @__PURE__ */ new Map();
960
+ for (const source of sources) {
961
+ let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source);
962
+ entries = entries.sort((a, b) => {
963
+ const cmp = a.title.localeCompare(b.title);
964
+ return sortDirection === "asc" ? cmp : -cmp;
965
+ });
966
+ const total = entries.length;
967
+ const pageEntries = entries.slice(offset, offset + limit);
968
+ const items = await this.loadTasks(pageEntries.map((e) => e.id));
969
+ if (sortBy !== "title" && items.length > 0) {
970
+ const sorted = sortTasksBy(items, sortBy, sortDirection);
971
+ bySource.set(source, {
972
+ items: sorted,
973
+ total,
974
+ hasMore: offset + limit < total,
975
+ offset,
976
+ limit
977
+ });
978
+ } else {
979
+ bySource.set(source, {
980
+ items,
981
+ total,
982
+ hasMore: offset + limit < total,
983
+ offset,
984
+ limit
985
+ });
986
+ }
987
+ }
988
+ return {
989
+ bySource,
990
+ sources
991
+ };
992
+ }
993
+ /**
994
+ * Load more tasks for a specific source (lazy loading)
995
+ *
996
+ * @param source - Source to load more from ("tasks" or "completed")
997
+ * @param currentOffset - Current offset (items already loaded)
998
+ * @param pagination - Pagination options
999
+ * @returns Paginated result for the source
1000
+ */
1001
+ async loadMoreForSource(source, currentOffset, pagination) {
1002
+ if (!this.lazyInitialized) {
1003
+ throw new Error("Core not lazy initialized. Call initializeLazy() first.");
1004
+ }
1005
+ const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
1006
+ const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
1007
+ let entries = Array.from(this.taskIndex.values()).filter((e) => e.source === source);
1008
+ entries = entries.sort((a, b) => {
1009
+ const cmp = a.title.localeCompare(b.title);
1010
+ return sortDirection === "asc" ? cmp : -cmp;
1011
+ });
1012
+ const total = entries.length;
1013
+ const pageEntries = entries.slice(currentOffset, currentOffset + limit);
1014
+ const items = await this.loadTasks(pageEntries.map((e) => e.id));
1015
+ return {
1016
+ items,
1017
+ total,
1018
+ hasMore: currentOffset + limit < total,
1019
+ offset: currentOffset,
1020
+ limit
1021
+ };
1022
+ }
809
1023
  /**
810
1024
  * Get the loaded configuration
811
1025
  *
@@ -863,6 +1077,99 @@ class Core {
863
1077
  this.ensureInitialized();
864
1078
  return this.tasks.get(id);
865
1079
  }
1080
+ /**
1081
+ * List tasks with pagination
1082
+ *
1083
+ * @param filter - Filter and pagination options
1084
+ * @returns Paginated result with tasks
1085
+ */
1086
+ listTasksPaginated(filter) {
1087
+ this.ensureInitialized();
1088
+ let tasks = this.applyFilters(Array.from(this.tasks.values()), filter);
1089
+ const pagination = (filter == null ? void 0 : filter.pagination) ?? {};
1090
+ const sortBy = pagination.sortBy ?? "title";
1091
+ const sortDirection = pagination.sortDirection ?? "asc";
1092
+ tasks = sortTasksBy(tasks, sortBy, sortDirection);
1093
+ const limit = pagination.limit ?? 10;
1094
+ const offset = pagination.offset ?? 0;
1095
+ const total = tasks.length;
1096
+ const items = tasks.slice(offset, offset + limit);
1097
+ return {
1098
+ items,
1099
+ total,
1100
+ hasMore: offset + limit < total,
1101
+ offset,
1102
+ limit
1103
+ };
1104
+ }
1105
+ /**
1106
+ * Get tasks by status with pagination per column
1107
+ *
1108
+ * @param pagination - Pagination options (applied per status)
1109
+ * @returns Paginated tasks grouped by status
1110
+ */
1111
+ getTasksByStatusPaginated(pagination) {
1112
+ this.ensureInitialized();
1113
+ const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
1114
+ const offset = (pagination == null ? void 0 : pagination.offset) ?? 0;
1115
+ const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
1116
+ const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
1117
+ const byStatus = /* @__PURE__ */ new Map();
1118
+ const allGrouped = /* @__PURE__ */ new Map();
1119
+ for (const status of this.config.statuses) {
1120
+ allGrouped.set(status, []);
1121
+ }
1122
+ for (const task of this.tasks.values()) {
1123
+ const list = allGrouped.get(task.status);
1124
+ if (list) {
1125
+ list.push(task);
1126
+ } else {
1127
+ allGrouped.set(task.status, [task]);
1128
+ }
1129
+ }
1130
+ for (const status of this.config.statuses) {
1131
+ let tasks = allGrouped.get(status) ?? [];
1132
+ tasks = sortTasksBy(tasks, sortBy, sortDirection);
1133
+ const total = tasks.length;
1134
+ const items = tasks.slice(offset, offset + limit);
1135
+ byStatus.set(status, {
1136
+ items,
1137
+ total,
1138
+ hasMore: offset + limit < total,
1139
+ offset,
1140
+ limit
1141
+ });
1142
+ }
1143
+ return {
1144
+ byStatus,
1145
+ statuses: this.config.statuses
1146
+ };
1147
+ }
1148
+ /**
1149
+ * Load more tasks for a specific status
1150
+ *
1151
+ * @param status - Status column to load more from
1152
+ * @param currentOffset - Current offset (items already loaded)
1153
+ * @param pagination - Pagination options (limit, sortBy, sortDirection)
1154
+ * @returns Paginated result for the status
1155
+ */
1156
+ loadMoreForStatus(status, currentOffset, pagination) {
1157
+ this.ensureInitialized();
1158
+ const limit = (pagination == null ? void 0 : pagination.limit) ?? 10;
1159
+ const sortBy = (pagination == null ? void 0 : pagination.sortBy) ?? "title";
1160
+ const sortDirection = (pagination == null ? void 0 : pagination.sortDirection) ?? "asc";
1161
+ let tasks = Array.from(this.tasks.values()).filter((t) => t.status === status);
1162
+ tasks = sortTasksBy(tasks, sortBy, sortDirection);
1163
+ const total = tasks.length;
1164
+ const items = tasks.slice(currentOffset, currentOffset + limit);
1165
+ return {
1166
+ items,
1167
+ total,
1168
+ hasMore: currentOffset + limit < total,
1169
+ offset: currentOffset,
1170
+ limit
1171
+ };
1172
+ }
866
1173
  /**
867
1174
  * Reload all tasks from disk
868
1175
  *
@@ -879,6 +1186,27 @@ class Core {
879
1186
  throw new Error("Core not initialized. Call initialize() first.");
880
1187
  }
881
1188
  }
1189
+ applyFilters(tasks, filter) {
1190
+ if (!filter)
1191
+ return tasks;
1192
+ let result = tasks;
1193
+ if (filter.status) {
1194
+ result = result.filter((t) => t.status === filter.status);
1195
+ }
1196
+ if (filter.assignee) {
1197
+ result = result.filter((t) => t.assignee.includes(filter.assignee));
1198
+ }
1199
+ if (filter.priority) {
1200
+ result = result.filter((t) => t.priority === filter.priority);
1201
+ }
1202
+ if (filter.labels && filter.labels.length > 0) {
1203
+ result = result.filter((t) => filter.labels.some((label) => t.labels.includes(label)));
1204
+ }
1205
+ if (filter.parentTaskId) {
1206
+ result = result.filter((t) => t.parentTaskId === filter.parentTaskId);
1207
+ }
1208
+ return result;
1209
+ }
882
1210
  async loadTasksFromDirectory(dir, source) {
883
1211
  const entries = await this.fs.readDir(dir);
884
1212
  for (const entry of entries) {
@@ -1019,17 +1347,26 @@ class PanelFileSystemAdapter {
1019
1347
  return path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\/+/g, "/");
1020
1348
  }
1021
1349
  }
1022
- const DEFAULT_STATUSES = ["To Do", "In Progress", "Done"];
1350
+ const SOURCE_DISPLAY_LABELS = {
1351
+ tasks: "Active",
1352
+ completed: "Completed"
1353
+ };
1354
+ const DEFAULT_SOURCES = ["tasks", "completed"];
1355
+ const DEFAULT_PAGE_SIZE = 10;
1023
1356
  function useKanbanData(options) {
1024
- const { context, actions } = options || {};
1357
+ const { context, actions, pageSize = DEFAULT_PAGE_SIZE } = options || {};
1025
1358
  const [tasks, setTasks] = useState([]);
1026
- const [statuses, setStatuses] = useState(DEFAULT_STATUSES);
1359
+ const [sources] = useState(DEFAULT_SOURCES);
1027
1360
  const [isLoading, setIsLoading] = useState(true);
1028
1361
  const [error, setError] = useState(null);
1029
1362
  const [isBacklogProject, setIsBacklogProject] = useState(false);
1030
- const [tasksByStatus, setTasksByStatus] = useState(
1363
+ const [tasksBySource, setTasksBySource] = useState(
1364
+ /* @__PURE__ */ new Map()
1365
+ );
1366
+ const [columnStates, setColumnStates] = useState(
1031
1367
  /* @__PURE__ */ new Map()
1032
1368
  );
1369
+ const coreRef = useRef(null);
1033
1370
  const activeFilePathRef = useRef(null);
1034
1371
  const contextRef = useRef(context);
1035
1372
  const actionsRef = useRef(actions);
@@ -1072,9 +1409,10 @@ function useKanbanData(options) {
1072
1409
  console.log("[useKanbanData] No context provided");
1073
1410
  setIsBacklogProject(false);
1074
1411
  setTasks([]);
1075
- setStatuses(DEFAULT_STATUSES);
1076
- setTasksByStatus(/* @__PURE__ */ new Map());
1412
+ setTasksBySource(/* @__PURE__ */ new Map());
1413
+ setColumnStates(/* @__PURE__ */ new Map());
1077
1414
  setIsLoading(false);
1415
+ coreRef.current = null;
1078
1416
  return;
1079
1417
  }
1080
1418
  setIsLoading(true);
@@ -1085,8 +1423,9 @@ function useKanbanData(options) {
1085
1423
  console.log("[useKanbanData] FileTree not available");
1086
1424
  setIsBacklogProject(false);
1087
1425
  setTasks([]);
1088
- setStatuses(DEFAULT_STATUSES);
1089
- setTasksByStatus(/* @__PURE__ */ new Map());
1426
+ setTasksBySource(/* @__PURE__ */ new Map());
1427
+ setColumnStates(/* @__PURE__ */ new Map());
1428
+ coreRef.current = null;
1090
1429
  return;
1091
1430
  }
1092
1431
  const files = fileTreeSlice.data.allFiles;
@@ -1104,36 +1443,139 @@ function useKanbanData(options) {
1104
1443
  console.log("[useKanbanData] Not a Backlog.md project");
1105
1444
  setIsBacklogProject(false);
1106
1445
  setTasks([]);
1107
- setStatuses(DEFAULT_STATUSES);
1108
- setTasksByStatus(/* @__PURE__ */ new Map());
1446
+ setTasksBySource(/* @__PURE__ */ new Map());
1447
+ setColumnStates(/* @__PURE__ */ new Map());
1448
+ coreRef.current = null;
1109
1449
  return;
1110
1450
  }
1111
- console.log("[useKanbanData] Loading Backlog.md data...");
1451
+ console.log("[useKanbanData] Loading Backlog.md data with lazy loading...");
1112
1452
  setIsBacklogProject(true);
1113
- await core.initialize();
1114
- const config = core.getConfig();
1115
- const grouped = core.getTasksByStatus();
1116
- const allTasks = core.listTasks();
1453
+ await core.initializeLazy(filePaths);
1454
+ coreRef.current = core;
1455
+ const paginatedResult = await core.getTasksBySourcePaginated({
1456
+ limit: pageSize,
1457
+ offset: 0,
1458
+ sortBy: "title",
1459
+ sortDirection: "asc"
1460
+ });
1461
+ const newTasksBySource = /* @__PURE__ */ new Map();
1462
+ const newColumnStates = /* @__PURE__ */ new Map();
1463
+ let allTasks = [];
1464
+ for (const source of paginatedResult.sources) {
1465
+ const columnResult = paginatedResult.bySource.get(source);
1466
+ if (columnResult) {
1467
+ newTasksBySource.set(source, columnResult.items);
1468
+ newColumnStates.set(source, {
1469
+ tasks: columnResult.items,
1470
+ total: columnResult.total,
1471
+ hasMore: columnResult.hasMore,
1472
+ isLoadingMore: false
1473
+ });
1474
+ allTasks = allTasks.concat(columnResult.items);
1475
+ } else {
1476
+ newTasksBySource.set(source, []);
1477
+ newColumnStates.set(source, {
1478
+ tasks: [],
1479
+ total: 0,
1480
+ hasMore: false,
1481
+ isLoadingMore: false
1482
+ });
1483
+ }
1484
+ }
1485
+ const totalTasks = Array.from(paginatedResult.bySource.values()).reduce(
1486
+ (sum, col) => sum + col.total,
1487
+ 0
1488
+ );
1117
1489
  console.log(
1118
- `[useKanbanData] Loaded ${allTasks.length} tasks with ${config.statuses.length} statuses`
1490
+ `[useKanbanData] Loaded ${allTasks.length}/${totalTasks} tasks (page size: ${pageSize})`
1119
1491
  );
1120
- setStatuses(config.statuses);
1121
1492
  setTasks(allTasks);
1122
- setTasksByStatus(grouped);
1493
+ setTasksBySource(newTasksBySource);
1494
+ setColumnStates(newColumnStates);
1123
1495
  } catch (err) {
1124
1496
  console.error("[useKanbanData] Failed to load Backlog.md data:", err);
1125
1497
  setError(err instanceof Error ? err.message : "Failed to load backlog data");
1126
1498
  setIsBacklogProject(false);
1127
1499
  setTasks([]);
1128
- setStatuses(DEFAULT_STATUSES);
1129
- setTasksByStatus(/* @__PURE__ */ new Map());
1500
+ setTasksBySource(/* @__PURE__ */ new Map());
1501
+ setColumnStates(/* @__PURE__ */ new Map());
1502
+ coreRef.current = null;
1130
1503
  } finally {
1131
1504
  setIsLoading(false);
1132
1505
  }
1133
- }, [context, actions, fetchFileContent]);
1506
+ }, [context, actions, fetchFileContent, pageSize]);
1134
1507
  useEffect(() => {
1135
1508
  loadBacklogData();
1136
1509
  }, [loadBacklogData]);
1510
+ const loadMore = useCallback(
1511
+ async (source) => {
1512
+ const core = coreRef.current;
1513
+ if (!core) {
1514
+ console.warn("[useKanbanData] Core not available for loadMore");
1515
+ return;
1516
+ }
1517
+ const currentState = columnStates.get(source);
1518
+ if (!currentState || !currentState.hasMore || currentState.isLoadingMore) {
1519
+ return;
1520
+ }
1521
+ setColumnStates((prev) => {
1522
+ const newStates = new Map(prev);
1523
+ const state = newStates.get(source);
1524
+ if (state) {
1525
+ newStates.set(source, { ...state, isLoadingMore: true });
1526
+ }
1527
+ return newStates;
1528
+ });
1529
+ try {
1530
+ const currentOffset = currentState.tasks.length;
1531
+ const result = await core.loadMoreForSource(
1532
+ source,
1533
+ currentOffset,
1534
+ {
1535
+ limit: pageSize,
1536
+ sortBy: "title",
1537
+ sortDirection: "asc"
1538
+ }
1539
+ );
1540
+ console.log(
1541
+ `[useKanbanData] Loaded ${result.items.length} more tasks for "${source}" (${currentOffset + result.items.length}/${result.total})`
1542
+ );
1543
+ setColumnStates((prev) => {
1544
+ const newStates = new Map(prev);
1545
+ const state = newStates.get(source);
1546
+ if (state) {
1547
+ const newTasks = [...state.tasks, ...result.items];
1548
+ newStates.set(source, {
1549
+ tasks: newTasks,
1550
+ total: result.total,
1551
+ hasMore: result.hasMore,
1552
+ isLoadingMore: false
1553
+ });
1554
+ }
1555
+ return newStates;
1556
+ });
1557
+ setTasksBySource((prev) => {
1558
+ const newMap = new Map(prev);
1559
+ const currentTasks = newMap.get(source) || [];
1560
+ newMap.set(source, [...currentTasks, ...result.items]);
1561
+ return newMap;
1562
+ });
1563
+ setTasks((prev) => [...prev, ...result.items]);
1564
+ } catch (err) {
1565
+ console.error(`[useKanbanData] Failed to load more for "${source}":`, err);
1566
+ setError(err instanceof Error ? err.message : "Failed to load more tasks");
1567
+ setColumnStates((prev) => {
1568
+ const newStates = new Map(prev);
1569
+ const state = newStates.get(source);
1570
+ if (state) {
1571
+ newStates.set(source, { ...state, isLoadingMore: false });
1572
+ }
1573
+ return newStates;
1574
+ });
1575
+ }
1576
+ },
1577
+ [columnStates, pageSize]
1578
+ );
1137
1579
  const refreshData = useCallback(async () => {
1138
1580
  await loadBacklogData();
1139
1581
  }, [loadBacklogData]);
@@ -1149,11 +1591,13 @@ function useKanbanData(options) {
1149
1591
  );
1150
1592
  return {
1151
1593
  tasks,
1152
- statuses,
1594
+ sources,
1153
1595
  isLoading,
1154
1596
  error,
1155
1597
  isBacklogProject,
1156
- tasksByStatus,
1598
+ tasksBySource,
1599
+ columnStates,
1600
+ loadMore,
1157
1601
  refreshData,
1158
1602
  updateTaskStatus
1159
1603
  };
@@ -1161,6 +1605,10 @@ function useKanbanData(options) {
1161
1605
  const KanbanColumn = ({
1162
1606
  status,
1163
1607
  tasks,
1608
+ total,
1609
+ hasMore = false,
1610
+ isLoadingMore = false,
1611
+ onLoadMore,
1164
1612
  onTaskClick
1165
1613
  }) => {
1166
1614
  const { theme: theme2 } = useTheme();
@@ -1176,6 +1624,7 @@ const KanbanColumn = ({
1176
1624
  return theme2.colors.border;
1177
1625
  }
1178
1626
  };
1627
+ const remaining = total !== void 0 ? total - tasks.length : 0;
1179
1628
  return /* @__PURE__ */ jsxs(
1180
1629
  "div",
1181
1630
  {
@@ -1224,13 +1673,13 @@ const KanbanColumn = ({
1224
1673
  padding: "2px 8px",
1225
1674
  borderRadius: theme2.radii[1]
1226
1675
  },
1227
- children: tasks.length
1676
+ children: total !== void 0 ? `${tasks.length}/${total}` : tasks.length
1228
1677
  }
1229
1678
  )
1230
1679
  ]
1231
1680
  }
1232
1681
  ),
1233
- /* @__PURE__ */ jsx(
1682
+ /* @__PURE__ */ jsxs(
1234
1683
  "div",
1235
1684
  {
1236
1685
  style: {
@@ -1241,131 +1690,190 @@ const KanbanColumn = ({
1241
1690
  overflowY: "auto",
1242
1691
  WebkitOverflowScrolling: "touch"
1243
1692
  },
1244
- children: tasks.map((task) => /* @__PURE__ */ jsxs(
1245
- "div",
1246
- {
1247
- onClick: () => onTaskClick == null ? void 0 : onTaskClick(task),
1248
- style: {
1249
- background: theme2.colors.surface,
1250
- borderRadius: theme2.radii[2],
1251
- padding: "12px",
1252
- border: `1px solid ${theme2.colors.border}`,
1253
- borderLeft: `4px solid ${getPriorityColor(task.priority)}`,
1254
- cursor: onTaskClick ? "pointer" : "default",
1255
- transition: "all 0.2s ease",
1256
- minHeight: "44px"
1257
- // Minimum touch target size for mobile
1258
- },
1259
- onMouseEnter: (e) => {
1260
- if (onTaskClick) {
1261
- e.currentTarget.style.transform = "translateY(-2px)";
1262
- e.currentTarget.style.boxShadow = `0 4px 8px ${theme2.colors.border}`;
1263
- }
1264
- },
1265
- onMouseLeave: (e) => {
1266
- if (onTaskClick) {
1267
- e.currentTarget.style.transform = "translateY(0)";
1268
- e.currentTarget.style.boxShadow = "none";
1269
- }
1270
- },
1271
- children: [
1272
- /* @__PURE__ */ jsx(
1273
- "h4",
1274
- {
1275
- style: {
1276
- margin: "0 0 8px 0",
1277
- fontSize: theme2.fontSizes[2],
1278
- color: theme2.colors.text,
1279
- fontWeight: theme2.fontWeights.medium
1280
- },
1281
- children: task.title
1693
+ children: [
1694
+ tasks.map((task) => /* @__PURE__ */ jsxs(
1695
+ "div",
1696
+ {
1697
+ onClick: () => onTaskClick == null ? void 0 : onTaskClick(task),
1698
+ style: {
1699
+ background: theme2.colors.surface,
1700
+ borderRadius: theme2.radii[2],
1701
+ padding: "12px",
1702
+ border: `1px solid ${theme2.colors.border}`,
1703
+ borderLeft: `4px solid ${getPriorityColor(task.priority)}`,
1704
+ cursor: onTaskClick ? "pointer" : "default",
1705
+ transition: "all 0.2s ease",
1706
+ minHeight: "44px"
1707
+ // Minimum touch target size for mobile
1708
+ },
1709
+ onMouseEnter: (e) => {
1710
+ if (onTaskClick) {
1711
+ e.currentTarget.style.transform = "translateY(-2px)";
1712
+ e.currentTarget.style.boxShadow = `0 4px 8px ${theme2.colors.border}`;
1282
1713
  }
1283
- ),
1284
- task.description && /* @__PURE__ */ jsx(
1285
- "p",
1286
- {
1287
- style: {
1288
- margin: "0 0 8px 0",
1289
- fontSize: theme2.fontSizes[1],
1290
- color: theme2.colors.textSecondary,
1291
- overflow: "hidden",
1292
- textOverflow: "ellipsis",
1293
- display: "-webkit-box",
1294
- WebkitLineClamp: 2,
1295
- WebkitBoxOrient: "vertical",
1296
- lineHeight: "1.4"
1297
- },
1298
- children: task.description
1714
+ },
1715
+ onMouseLeave: (e) => {
1716
+ if (onTaskClick) {
1717
+ e.currentTarget.style.transform = "translateY(0)";
1718
+ e.currentTarget.style.boxShadow = "none";
1299
1719
  }
1300
- ),
1301
- task.labels && task.labels.length > 0 && /* @__PURE__ */ jsx(
1302
- "div",
1303
- {
1304
- style: {
1305
- display: "flex",
1306
- gap: "4px",
1307
- flexWrap: "wrap",
1308
- marginBottom: "8px"
1309
- },
1310
- children: task.labels.map((label) => /* @__PURE__ */ jsx(
1311
- "span",
1312
- {
1313
- style: {
1314
- fontSize: theme2.fontSizes[0],
1315
- color: theme2.colors.primary,
1316
- background: `${theme2.colors.primary}20`,
1317
- padding: "2px 8px",
1318
- borderRadius: theme2.radii[1],
1319
- fontWeight: theme2.fontWeights.medium
1320
- },
1321
- children: label
1720
+ },
1721
+ children: [
1722
+ /* @__PURE__ */ jsx(
1723
+ "h4",
1724
+ {
1725
+ style: {
1726
+ margin: "0 0 8px 0",
1727
+ fontSize: theme2.fontSizes[2],
1728
+ color: theme2.colors.text,
1729
+ fontWeight: theme2.fontWeights.medium
1322
1730
  },
1323
- label
1324
- ))
1325
- }
1326
- ),
1327
- /* @__PURE__ */ jsxs(
1328
- "div",
1329
- {
1330
- style: {
1331
- display: "flex",
1332
- alignItems: "center",
1333
- justifyContent: "space-between",
1334
- fontSize: theme2.fontSizes[0],
1335
- color: theme2.colors.textMuted
1336
- },
1337
- children: [
1338
- /* @__PURE__ */ jsx(
1339
- "span",
1340
- {
1341
- style: {
1342
- fontFamily: theme2.fonts.monospace
1343
- },
1344
- children: task.id
1345
- }
1346
- ),
1347
- task.assignee && task.assignee.length > 0 && /* @__PURE__ */ jsxs(
1731
+ children: task.title
1732
+ }
1733
+ ),
1734
+ task.description && /* @__PURE__ */ jsx(
1735
+ "p",
1736
+ {
1737
+ style: {
1738
+ margin: "0 0 8px 0",
1739
+ fontSize: theme2.fontSizes[1],
1740
+ color: theme2.colors.textSecondary,
1741
+ overflow: "hidden",
1742
+ textOverflow: "ellipsis",
1743
+ display: "-webkit-box",
1744
+ WebkitLineClamp: 2,
1745
+ WebkitBoxOrient: "vertical",
1746
+ lineHeight: "1.4"
1747
+ },
1748
+ children: task.description
1749
+ }
1750
+ ),
1751
+ task.labels && task.labels.length > 0 && /* @__PURE__ */ jsx(
1752
+ "div",
1753
+ {
1754
+ style: {
1755
+ display: "flex",
1756
+ gap: "4px",
1757
+ flexWrap: "wrap",
1758
+ marginBottom: "8px"
1759
+ },
1760
+ children: task.labels.map((label) => /* @__PURE__ */ jsx(
1348
1761
  "span",
1349
1762
  {
1350
1763
  style: {
1351
- color: theme2.colors.textSecondary
1764
+ fontSize: theme2.fontSizes[0],
1765
+ color: theme2.colors.primary,
1766
+ background: `${theme2.colors.primary}20`,
1767
+ padding: "2px 8px",
1768
+ borderRadius: theme2.radii[1],
1769
+ fontWeight: theme2.fontWeights.medium
1352
1770
  },
1353
- children: [
1354
- task.assignee.length,
1355
- " assignee",
1356
- task.assignee.length !== 1 ? "s" : ""
1357
- ]
1358
- }
1359
- )
1360
- ]
1771
+ children: label
1772
+ },
1773
+ label
1774
+ ))
1775
+ }
1776
+ ),
1777
+ /* @__PURE__ */ jsxs(
1778
+ "div",
1779
+ {
1780
+ style: {
1781
+ display: "flex",
1782
+ alignItems: "center",
1783
+ justifyContent: "space-between",
1784
+ fontSize: theme2.fontSizes[0],
1785
+ color: theme2.colors.textMuted
1786
+ },
1787
+ children: [
1788
+ /* @__PURE__ */ jsx(
1789
+ "span",
1790
+ {
1791
+ style: {
1792
+ fontFamily: theme2.fonts.monospace
1793
+ },
1794
+ children: task.id
1795
+ }
1796
+ ),
1797
+ task.assignee && task.assignee.length > 0 && /* @__PURE__ */ jsxs(
1798
+ "span",
1799
+ {
1800
+ style: {
1801
+ color: theme2.colors.textSecondary
1802
+ },
1803
+ children: [
1804
+ task.assignee.length,
1805
+ " assignee",
1806
+ task.assignee.length !== 1 ? "s" : ""
1807
+ ]
1808
+ }
1809
+ )
1810
+ ]
1811
+ }
1812
+ )
1813
+ ]
1814
+ },
1815
+ task.id
1816
+ )),
1817
+ hasMore && onLoadMore && /* @__PURE__ */ jsx(
1818
+ "button",
1819
+ {
1820
+ onClick: onLoadMore,
1821
+ disabled: isLoadingMore,
1822
+ style: {
1823
+ background: theme2.colors.background,
1824
+ border: `1px dashed ${theme2.colors.border}`,
1825
+ borderRadius: theme2.radii[2],
1826
+ padding: "12px",
1827
+ cursor: isLoadingMore ? "wait" : "pointer",
1828
+ color: theme2.colors.textSecondary,
1829
+ fontSize: theme2.fontSizes[1],
1830
+ fontWeight: theme2.fontWeights.medium,
1831
+ transition: "all 0.2s ease",
1832
+ minHeight: "44px",
1833
+ display: "flex",
1834
+ alignItems: "center",
1835
+ justifyContent: "center",
1836
+ gap: "8px"
1837
+ },
1838
+ onMouseEnter: (e) => {
1839
+ if (!isLoadingMore) {
1840
+ e.currentTarget.style.background = theme2.colors.backgroundSecondary;
1841
+ e.currentTarget.style.borderColor = theme2.colors.primary;
1842
+ e.currentTarget.style.color = theme2.colors.primary;
1361
1843
  }
1362
- )
1363
- ]
1364
- },
1365
- task.id
1366
- ))
1844
+ },
1845
+ onMouseLeave: (e) => {
1846
+ e.currentTarget.style.background = theme2.colors.background;
1847
+ e.currentTarget.style.borderColor = theme2.colors.border;
1848
+ e.currentTarget.style.color = theme2.colors.textSecondary;
1849
+ },
1850
+ children: isLoadingMore ? /* @__PURE__ */ jsxs(Fragment, { children: [
1851
+ /* @__PURE__ */ jsx(
1852
+ "span",
1853
+ {
1854
+ style: {
1855
+ display: "inline-block",
1856
+ width: "14px",
1857
+ height: "14px",
1858
+ border: `2px solid ${theme2.colors.border}`,
1859
+ borderTopColor: theme2.colors.primary,
1860
+ borderRadius: "50%",
1861
+ animation: "spin 1s linear infinite"
1862
+ }
1863
+ }
1864
+ ),
1865
+ "Loading..."
1866
+ ] }) : `Load more (${remaining} remaining)`
1867
+ }
1868
+ )
1869
+ ]
1367
1870
  }
1368
- )
1871
+ ),
1872
+ /* @__PURE__ */ jsx("style", { children: `
1873
+ @keyframes spin {
1874
+ to { transform: rotate(360deg); }
1875
+ }
1876
+ ` })
1369
1877
  ]
1370
1878
  }
1371
1879
  );
@@ -1626,9 +2134,10 @@ const KanbanPanelContent = ({
1626
2134
  var _a, _b;
1627
2135
  const { theme: theme2 } = useTheme();
1628
2136
  const [_selectedTask, setSelectedTask] = useState(null);
1629
- const { statuses, tasksByStatus, error, isBacklogProject, refreshData } = useKanbanData({
2137
+ const { sources, tasksBySource, columnStates, loadMore, error, isBacklogProject, refreshData } = useKanbanData({
1630
2138
  context,
1631
- actions
2139
+ actions,
2140
+ pageSize: 3
1632
2141
  });
1633
2142
  const handleTaskClick = (task) => {
1634
2143
  setSelectedTask(task);
@@ -1772,16 +2281,21 @@ const KanbanPanelContent = ({
1772
2281
  WebkitOverflowScrolling: "touch"
1773
2282
  // Smooth scrolling on iOS
1774
2283
  },
1775
- children: statuses.map((status) => {
1776
- const columnTasks = tasksByStatus.get(status) || [];
2284
+ children: sources.map((source) => {
2285
+ const columnTasks = tasksBySource.get(source) || [];
2286
+ const columnState = columnStates.get(source);
1777
2287
  return /* @__PURE__ */ jsx(
1778
2288
  KanbanColumn,
1779
2289
  {
1780
- status,
2290
+ status: SOURCE_DISPLAY_LABELS[source],
1781
2291
  tasks: columnTasks,
2292
+ total: columnState == null ? void 0 : columnState.total,
2293
+ hasMore: columnState == null ? void 0 : columnState.hasMore,
2294
+ isLoadingMore: columnState == null ? void 0 : columnState.isLoadingMore,
2295
+ onLoadMore: () => loadMore(source),
1782
2296
  onTaskClick: handleTaskClick
1783
2297
  },
1784
- status
2298
+ source
1785
2299
  );
1786
2300
  })
1787
2301
  }