@burdenoff/planmagnet-fe-sdk 2026.706.6 → 2026.708.1

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.
package/dist/index.js CHANGED
@@ -6,12 +6,13 @@ import { onError } from '@apollo/client/link/error';
6
6
  import { CombinedGraphQLErrors } from '@apollo/client/errors';
7
7
  import { jsx, jsxs } from 'react/jsx-runtime';
8
8
  import { gql } from '@apollo/client';
9
- import { CircleX, CircleCheckBig, CircleDotDashed, CircleDot, Circle, CircleDashed, User, Link2, CalendarDays, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Flag, Plus, CalendarRange, List, LayoutGrid, Search, Trash2 } from 'lucide-react';
9
+ import { CircleX, CircleCheckBig, CircleDotDashed, CircleDot, Circle, CircleDashed, User, Link2, CalendarDays, Trash2, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Flag, Plus, CalendarRange, List, LayoutGrid, Search, Bookmark, Check, Pencil, SmilePlus } from 'lucide-react';
10
10
  import { clsx } from 'clsx';
11
11
  import { twMerge } from 'tailwind-merge';
12
- import { addWeeks, startOfWeek, endOfWeek, eachDayOfInterval, addMonths, startOfMonth, endOfMonth, format, isToday, isSameMonth, startOfDay, parseISO, isValid } from 'date-fns';
12
+ import { startOfWeek, format, addDays, addWeeks, endOfWeek, eachDayOfInterval, addMonths, startOfMonth, endOfMonth, isToday, isSameMonth, startOfDay, parseISO, isValid } from 'date-fns';
13
13
  import { cva } from 'class-variance-authority';
14
14
  import { Card, CardContent } from '@burdenoff/fe-libs/ui';
15
+ import { enUS, ta, ar, es, zhCN, hi } from 'date-fns/locale';
15
16
 
16
17
  var clientInstance = null;
17
18
  function buildCache() {
@@ -281,6 +282,12 @@ var WORK_ITEM_SUMMARY_FIELDS = gql`
281
282
  storyPoints
282
283
  dueDate
283
284
  labels
285
+ # Board reorder ordering. sortOrder maps a Prisma \`position\` column and is
286
+ # unsafe to SELECT BACK FROM A MUTATION (serialization crash), but SAFE on
287
+ # this QUERY read path — a WorkItem.sortOrder field resolver maps position →
288
+ # sortOrder. The board sorts cards by this ascending so a drag-to-reorder
289
+ # persists across refetch (the list query itself orders by createdAt).
290
+ sortOrder
284
291
  }
285
292
  `;
286
293
  var WORK_ITEM_FIELDS = gql`
@@ -405,10 +412,11 @@ var UPDATE_WORK_ITEM = gql`
405
412
  var MOVE_WORK_ITEM = gql`
406
413
  mutation MoveWorkItem($id: ID!, $input: PlanmagnetMoveWorkItemInput!) {
407
414
  planmagnetMoveWorkItem(id: $id, input: $input) {
408
- ...WorkItemFields
415
+ id
416
+ status
417
+ sprintId
409
418
  }
410
419
  }
411
- ${WORK_ITEM_FIELDS}
412
420
  `;
413
421
  var WORK_ITEM_LINK_FIELDS = gql`
414
422
  fragment WorkItemLinkFields on WorkItemLink {
@@ -520,6 +528,16 @@ var DELETE_WORK_ITEM = gql`
520
528
  planmagnetDeleteWorkItem(id: $id)
521
529
  }
522
530
  `;
531
+ var BULK_UPDATE_WORK_ITEMS = gql`
532
+ mutation BulkUpdateWorkItems($input: PlanmagnetBulkUpdateWorkItemsInput!) {
533
+ planmagnetBulkUpdateWorkItems(input: $input) {
534
+ id
535
+ status
536
+ priority
537
+ assigneeId
538
+ }
539
+ }
540
+ `;
523
541
  var ENTITY_COMMENT_FIELDS = gql`
524
542
  fragment EntityCommentFields on EntityComment {
525
543
  id
@@ -538,6 +556,11 @@ var ENTITY_COMMENT_FIELDS = gql`
538
556
  mentions {
539
557
  mentionedUserId
540
558
  }
559
+ reactionSummary {
560
+ emoji
561
+ count
562
+ userIds
563
+ }
541
564
  }
542
565
  `;
543
566
  var GET_ENTITY_COMMENTS = gql`
@@ -564,6 +587,29 @@ var DELETE_ENTITY_COMMENT = gql`
564
587
  deleteEntityComment(input: $input)
565
588
  }
566
589
  `;
590
+ var UPDATE_ENTITY_COMMENT = gql`
591
+ mutation UpdateEntityComment($input: UpdateEntityCommentInput!) {
592
+ updateEntityComment(input: $input) {
593
+ ...EntityCommentFields
594
+ }
595
+ }
596
+ ${ENTITY_COMMENT_FIELDS}
597
+ `;
598
+ var ADD_ENTITY_COMMENT_REACTION = gql`
599
+ mutation AddEntityCommentReaction($input: AddReactionInput!) {
600
+ addEntityCommentReaction(input: $input) {
601
+ id
602
+ commentId
603
+ emoji
604
+ userId
605
+ }
606
+ }
607
+ `;
608
+ var REMOVE_ENTITY_COMMENT_REACTION = gql`
609
+ mutation RemoveEntityCommentReaction($input: RemoveReactionInput!) {
610
+ removeEntityCommentReaction(input: $input)
611
+ }
612
+ `;
567
613
  function cn(...inputs) {
568
614
  return twMerge(clsx(inputs));
569
615
  }
@@ -769,6 +815,22 @@ var DEFAULT_TASK_LABELS = {
769
815
  allTypes: "All types",
770
816
  newTask: "New task",
771
817
  myTasks: "My tasks",
818
+ savedViews: "Views",
819
+ saveView: "Save current view",
820
+ viewName: "View name",
821
+ saveViewAction: "Save",
822
+ deleteView: "Delete",
823
+ noSavedViews: "No saved views",
824
+ defaultViewName: "Default",
825
+ keyboardShortcuts: "Keyboard shortcuts",
826
+ shortcutNewTask: "New task",
827
+ shortcutSearch: "Focus search",
828
+ shortcutBoardView: "Board view",
829
+ shortcutListView: "List view",
830
+ shortcutTimelineView: "Timeline view",
831
+ shortcutMyTasks: "Toggle My tasks",
832
+ shortcutHelp: "Toggle this help",
833
+ shortcutClose: "Close overlay",
772
834
  noTasks: "No tasks",
773
835
  noTasksFound: "No tasks found.",
774
836
  dropHere: "Drop here",
@@ -788,6 +850,13 @@ var DEFAULT_TASK_LABELS = {
788
850
  month: "Month",
789
851
  tasksSingular: "task",
790
852
  tasksPlural: "tasks",
853
+ bulkSelected: "{n} selected",
854
+ bulkSetStatus: "Set status",
855
+ bulkSetPriority: "Set priority",
856
+ bulkAssign: "Assign",
857
+ bulkDelete: "Delete",
858
+ bulkClear: "Clear",
859
+ bulkApplying: "Applying\u2026",
791
860
  colTask: "Task",
792
861
  colStatus: "Status",
793
862
  colPriority: "Priority",
@@ -826,8 +895,13 @@ var DEFAULT_TASK_LABELS = {
826
895
  noComments: "No comments yet",
827
896
  you: "You",
828
897
  editedTag: "(edited)",
898
+ editComment: "Edit",
899
+ saveComment: "Save",
900
+ addReaction: "React",
829
901
  failedToComment: "Failed to post comment",
830
902
  failedToDeleteComment: "Failed to delete comment",
903
+ failedToEditComment: "Failed to edit comment",
904
+ failedToReact: "Failed to update reaction",
831
905
  reply: "Reply",
832
906
  replyPlaceholder: "Write a reply\u2026",
833
907
  repliesCount: "{n} replies",
@@ -845,6 +919,7 @@ var DEFAULT_TASK_LABELS = {
845
919
  projectRequired: "A projectId is required to create a task.",
846
920
  errorLoading: "Error loading tasks",
847
921
  moveFailed: "Couldn't move the task. Please try again.",
922
+ reorderFailed: "Couldn't reorder the task. Please try again.",
848
923
  failedToCreate: "Failed to create task",
849
924
  failedToSave: "Failed to save",
850
925
  failedToDelete: "Failed to delete",
@@ -1043,6 +1118,7 @@ function TaskCard({
1043
1118
  draggable,
1044
1119
  onDragStart,
1045
1120
  onDragEnd,
1121
+ onDragOver,
1046
1122
  dragging,
1047
1123
  resolveAssignee,
1048
1124
  links,
@@ -1077,6 +1153,7 @@ function TaskCard({
1077
1153
  draggable,
1078
1154
  onDragStart: draggable ? (e) => onDragStart?.(task, e) : void 0,
1079
1155
  onDragEnd: draggable ? (e) => onDragEnd?.(task, e) : void 0,
1156
+ onDragOver: draggable ? onDragOver : void 0,
1080
1157
  onClick: onClick ? () => onClick(task) : void 0,
1081
1158
  onKeyDown: handleKey,
1082
1159
  className: cn(
@@ -1172,6 +1249,13 @@ function useWorkItemLinks(workspaceId, sourceProduct, limit = 500) {
1172
1249
  }, [data]);
1173
1250
  return { linksByItem, loading };
1174
1251
  }
1252
+ function bySortOrder(tasks) {
1253
+ return tasks.map((task, index) => ({ task, index })).sort((a, b) => {
1254
+ const ao = a.task.sortOrder ?? Number.MAX_SAFE_INTEGER;
1255
+ const bo = b.task.sortOrder ?? Number.MAX_SAFE_INTEGER;
1256
+ return ao === bo ? a.index - b.index : ao - bo;
1257
+ }).map((entry) => entry.task);
1258
+ }
1175
1259
  function TaskBoardCore({
1176
1260
  projectId,
1177
1261
  columns = DEFAULT_BOARD_COLUMNS,
@@ -1198,9 +1282,11 @@ function TaskBoardCore({
1198
1282
  const { linksByItem } = useWorkItemLinks(workspaceId, linkSourceProduct);
1199
1283
  const [draggingId, setDraggingId] = useState(null);
1200
1284
  const [dragOver, setDragOver] = useState(null);
1285
+ const [dropIndex, setDropIndex] = useState(null);
1201
1286
  const [moveError, setMoveError] = useState(null);
1202
1287
  const [pendingMove, setPendingMove] = useState(null);
1203
1288
  const [updateItem] = useMutation(UPDATE_WORK_ITEM);
1289
+ const [moveItem] = useMutation(MOVE_WORK_ITEM);
1204
1290
  const { data, loading, error } = useQuery(
1205
1291
  LIST_WORK_ITEMS,
1206
1292
  {
@@ -1229,6 +1315,44 @@ function TaskBoardCore({
1229
1315
  },
1230
1316
  [client, updateItem, L]
1231
1317
  );
1318
+ const reorderTask = useCallback(
1319
+ (columnTasks, fromIndex, toIndex) => {
1320
+ if (fromIndex === toIndex || fromIndex < 0) return;
1321
+ const ordered = columnTasks.slice();
1322
+ const [moved] = ordered.splice(fromIndex, 1);
1323
+ if (!moved) return;
1324
+ const insertAt = Math.max(0, Math.min(toIndex, ordered.length));
1325
+ ordered.splice(insertAt, 0, moved);
1326
+ const changed = [];
1327
+ ordered.forEach((task, index) => {
1328
+ const prev = task.sortOrder ?? null;
1329
+ if (prev !== index) {
1330
+ changed.push({ id: task.id, from: prev, to: index });
1331
+ }
1332
+ });
1333
+ if (changed.length === 0) return;
1334
+ setMoveError(null);
1335
+ const setOrder = (id, value) => {
1336
+ const cacheId = client.cache.identify({ __typename: "WorkItem", id });
1337
+ if (cacheId) {
1338
+ client.cache.modify({
1339
+ id: cacheId,
1340
+ fields: { sortOrder: () => value }
1341
+ });
1342
+ }
1343
+ };
1344
+ for (const c of changed) setOrder(c.id, c.to);
1345
+ Promise.all(
1346
+ changed.map(
1347
+ (c) => moveItem({ variables: { id: c.id, input: { sortOrder: c.to } } })
1348
+ )
1349
+ ).catch(() => {
1350
+ for (const c of changed) setOrder(c.id, c.from);
1351
+ setMoveError(L.reorderFailed);
1352
+ });
1353
+ },
1354
+ [client, moveItem, L]
1355
+ );
1232
1356
  if (!workspaceId) {
1233
1357
  return /* @__PURE__ */ jsx("div", { className: cn("p-6 text-sm text-destructive", className), children: "TaskBoard: no workspaceId provided (prop or PlanMagnetProvider)." });
1234
1358
  }
@@ -1255,18 +1379,41 @@ function TaskBoardCore({
1255
1379
  return true;
1256
1380
  });
1257
1381
  const canDrag = !readonly;
1382
+ const columnTasksFor = (status) => bySortOrder(filtered.filter((t) => t.status === status));
1258
1383
  const onColumnDragOver = (status) => (e) => {
1259
1384
  if (!canDrag || !draggingId) return;
1260
1385
  e.preventDefault();
1261
1386
  if (dragOver !== status) setDragOver(status);
1387
+ if (dropIndex === null) setDropIndex(columnTasksFor(status).length);
1388
+ };
1389
+ const onCardDragOver = (status, index) => (e) => {
1390
+ if (!canDrag || !draggingId) return;
1391
+ e.preventDefault();
1392
+ e.stopPropagation();
1393
+ if (dragOver !== status) setDragOver(status);
1394
+ const rect = e.currentTarget.getBoundingClientRect();
1395
+ const after = e.clientY - rect.top > rect.height / 2;
1396
+ const next = after ? index + 1 : index;
1397
+ if (dropIndex !== next) setDropIndex(next);
1398
+ };
1399
+ const clearDrag = () => {
1400
+ setDragOver(null);
1401
+ setDraggingId(null);
1402
+ setDropIndex(null);
1262
1403
  };
1263
1404
  const onColumnDrop = (status) => (e) => {
1264
1405
  if (!canDrag) return;
1265
1406
  e.preventDefault();
1266
1407
  const task = tasks.find((t) => t.id === draggingId);
1267
- setDragOver(null);
1268
- setDraggingId(null);
1269
- if (!task || task.status === status) return;
1408
+ const targetIndex = dropIndex;
1409
+ clearDrag();
1410
+ if (!task) return;
1411
+ if (task.status === status) {
1412
+ const colTasks = columnTasksFor(status);
1413
+ const fromIndex = colTasks.findIndex((t) => t.id === task.id);
1414
+ if (targetIndex !== null) reorderTask(colTasks, fromIndex, targetIndex);
1415
+ return;
1416
+ }
1270
1417
  if (confirmMove) setPendingMove({ task, toStatus: status });
1271
1418
  else moveTask(task, status);
1272
1419
  };
@@ -1281,8 +1428,9 @@ function TaskBoardCore({
1281
1428
  ] }),
1282
1429
  moveError && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: moveError }),
1283
1430
  /* @__PURE__ */ jsx("div", { className: "flex gap-3 overflow-x-auto pb-2", children: columns.map((col) => {
1284
- const colTasks = filtered.filter((t) => t.status === col.status);
1431
+ const colTasks = columnTasksFor(col.status);
1285
1432
  const isOver = dragOver === col.status;
1433
+ const showDropLine = canDrag && isOver && dropIndex !== null && colTasks.some((t) => t.id === draggingId);
1286
1434
  return /* @__PURE__ */ jsxs(
1287
1435
  "div",
1288
1436
  {
@@ -1301,29 +1449,45 @@ function TaskBoardCore({
1301
1449
  ] }),
1302
1450
  /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-1.5 text-xs font-medium text-muted-foreground", children: colTasks.length })
1303
1451
  ] }),
1304
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-2 pb-2", children: colTasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border/70 px-3 py-6 text-center text-xs text-muted-foreground", children: isOver ? L.dropHere : L.noTasks }) : colTasks.map((task) => /* @__PURE__ */ jsx(
1305
- TaskCard,
1306
- {
1307
- task,
1308
- hidden,
1309
- onClick: onTaskClick,
1310
- draggable: canDrag,
1311
- dragging: draggingId === task.id,
1312
- resolveAssignee,
1313
- links: linksByItem.get(task.id),
1314
- renderLinkIcon,
1315
- onDragStart: (t, e) => {
1316
- setDraggingId(t.id);
1317
- e.dataTransfer.effectAllowed = "move";
1318
- e.dataTransfer.setData("text/plain", t.id);
1319
- },
1320
- onDragEnd: () => {
1321
- setDraggingId(null);
1322
- setDragOver(null);
1452
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-2 pb-2", children: colTasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border/70 px-3 py-6 text-center text-xs text-muted-foreground", children: isOver ? L.dropHere : L.noTasks }) : colTasks.map((task, index) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1453
+ showDropLine && dropIndex === index && /* @__PURE__ */ jsx(
1454
+ "div",
1455
+ {
1456
+ "aria-hidden": "true",
1457
+ "data-testid": "reorder-drop-line",
1458
+ className: "h-0.5 rounded-full bg-primary"
1459
+ }
1460
+ ),
1461
+ /* @__PURE__ */ jsx(
1462
+ TaskCard,
1463
+ {
1464
+ task,
1465
+ hidden,
1466
+ onClick: onTaskClick,
1467
+ draggable: canDrag,
1468
+ dragging: draggingId === task.id,
1469
+ resolveAssignee,
1470
+ links: linksByItem.get(task.id),
1471
+ renderLinkIcon,
1472
+ onDragOver: onCardDragOver(col.status, index),
1473
+ onDragStart: (t, e) => {
1474
+ setDraggingId(t.id);
1475
+ setDropIndex(null);
1476
+ e.dataTransfer.effectAllowed = "move";
1477
+ e.dataTransfer.setData("text/plain", t.id);
1478
+ },
1479
+ onDragEnd: clearDrag
1480
+ }
1481
+ ),
1482
+ showDropLine && index === colTasks.length - 1 && dropIndex >= colTasks.length && /* @__PURE__ */ jsx(
1483
+ "div",
1484
+ {
1485
+ "aria-hidden": "true",
1486
+ "data-testid": "reorder-drop-line",
1487
+ className: "h-0.5 rounded-full bg-primary"
1323
1488
  }
1324
- },
1325
- task.id
1326
- )) })
1489
+ )
1490
+ ] }, task.id)) })
1327
1491
  ]
1328
1492
  },
1329
1493
  col.status
@@ -1723,6 +1887,27 @@ var statusRank = {
1723
1887
  DONE: 5,
1724
1888
  CANCELLED: 6
1725
1889
  };
1890
+ var bulkFieldClass = "rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50";
1891
+ function BulkCheckbox({
1892
+ checked,
1893
+ indeterminate = false,
1894
+ ...props
1895
+ }) {
1896
+ const ref = useRef(null);
1897
+ useEffect(() => {
1898
+ if (ref.current) ref.current.indeterminate = indeterminate;
1899
+ }, [indeterminate]);
1900
+ return /* @__PURE__ */ jsx(
1901
+ "input",
1902
+ {
1903
+ ref,
1904
+ type: "checkbox",
1905
+ checked,
1906
+ className: "h-4 w-4 cursor-pointer accent-primary disabled:cursor-not-allowed disabled:opacity-50",
1907
+ ...props
1908
+ }
1909
+ );
1910
+ }
1726
1911
  function columnLabel(col, L) {
1727
1912
  switch (col) {
1728
1913
  case "key":
@@ -1773,6 +1958,8 @@ function TaskListCore({
1773
1958
  linkSourceProduct,
1774
1959
  renderLinkIcon,
1775
1960
  limit = 200,
1961
+ selectable = false,
1962
+ assigneeOptions,
1776
1963
  className,
1777
1964
  workspaceId: workspaceIdProp
1778
1965
  }) {
@@ -1794,7 +1981,7 @@ function TaskListCore({
1794
1981
  if (search) f.search = search;
1795
1982
  return f;
1796
1983
  }, [projectId, status, type, priority, assigneeId, search]);
1797
- const { data, loading, error } = useQuery(
1984
+ const { data, loading, error, refetch } = useQuery(
1798
1985
  LIST_WORK_ITEMS,
1799
1986
  {
1800
1987
  variables: {
@@ -1812,6 +1999,66 @@ function TaskListCore({
1812
1999
  const key = sort.by;
1813
2000
  return [...items].sort((a, b) => dir * compareTasks(a, b, key));
1814
2001
  }, [data, sort]);
2002
+ const [selected, setSelected] = useState(
2003
+ () => /* @__PURE__ */ new Set()
2004
+ );
2005
+ const rowsKey = useMemo(() => rows.map((r) => r.id).join(","), [rows]);
2006
+ useEffect(() => {
2007
+ setSelected((prev) => prev.size ? /* @__PURE__ */ new Set() : prev);
2008
+ }, [rowsKey]);
2009
+ const [bulkUpdate, { loading: bulkUpdating }] = useMutation(
2010
+ BULK_UPDATE_WORK_ITEMS
2011
+ );
2012
+ const [bulkDelete, { loading: bulkDeleting }] = useMutation(DELETE_WORK_ITEM);
2013
+ const applying = bulkUpdating || bulkDeleting;
2014
+ const [bulkError, setBulkError] = useState(null);
2015
+ const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id));
2016
+ const someSelected = selected.size > 0 && !allSelected;
2017
+ const toggleRow = (id) => {
2018
+ setSelected((prev) => {
2019
+ const next = new Set(prev);
2020
+ if (next.has(id)) next.delete(id);
2021
+ else next.add(id);
2022
+ return next;
2023
+ });
2024
+ };
2025
+ const toggleAll = () => {
2026
+ setSelected(
2027
+ (prev) => prev.size === rows.length ? /* @__PURE__ */ new Set() : new Set(rows.map((r) => r.id))
2028
+ );
2029
+ };
2030
+ const clearSelection = () => {
2031
+ setSelected(/* @__PURE__ */ new Set());
2032
+ setBulkError(null);
2033
+ };
2034
+ const applyBulk = async (patch) => {
2035
+ if (selected.size === 0 || applying) return;
2036
+ setBulkError(null);
2037
+ try {
2038
+ await bulkUpdate({
2039
+ variables: { input: { ids: [...selected], ...patch } }
2040
+ });
2041
+ } catch {
2042
+ setBulkError(L.failedToSave);
2043
+ return;
2044
+ }
2045
+ await refetch().catch(() => {
2046
+ });
2047
+ clearSelection();
2048
+ };
2049
+ const applyBulkDelete = async () => {
2050
+ if (selected.size === 0 || applying) return;
2051
+ setBulkError(null);
2052
+ const results = await Promise.allSettled(
2053
+ [...selected].map((id) => bulkDelete({ variables: { id } }))
2054
+ );
2055
+ await refetch().catch(() => {
2056
+ });
2057
+ clearSelection();
2058
+ if (results.some((r) => r.status === "rejected")) {
2059
+ setBulkError(L.failedToDelete);
2060
+ }
2061
+ };
1815
2062
  const toggleSort = (col) => {
1816
2063
  const key = COLUMN_SORT[col];
1817
2064
  if (!key) return;
@@ -1832,6 +2079,7 @@ function TaskListCore({
1832
2079
  error.message
1833
2080
  ] });
1834
2081
  }
2082
+ const colCount = columns.length + (selectable ? 1 : 0);
1835
2083
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
1836
2084
  !hideHeader && /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between", children: [
1837
2085
  /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-foreground", children: L.list }),
@@ -1841,56 +2089,160 @@ function TaskListCore({
1841
2089
  rows.length === 1 ? L.tasksSingular : L.tasksPlural
1842
2090
  ] })
1843
2091
  ] }),
2092
+ selectable && selected.size > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 rounded-lg border border-border bg-muted/50 px-3 py-2 text-sm", children: [
2093
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: L.bulkSelected.replace("{n}", String(selected.size)) }),
2094
+ /* @__PURE__ */ jsxs(
2095
+ "select",
2096
+ {
2097
+ "aria-label": L.bulkSetStatus,
2098
+ disabled: applying,
2099
+ value: "",
2100
+ onChange: (e) => {
2101
+ const v = e.target.value;
2102
+ if (v) void applyBulk({ status: v });
2103
+ },
2104
+ className: bulkFieldClass,
2105
+ children: [
2106
+ /* @__PURE__ */ jsx("option", { value: "", children: L.bulkSetStatus }),
2107
+ TASK_STATUSES.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: L.status[s] }, s))
2108
+ ]
2109
+ }
2110
+ ),
2111
+ /* @__PURE__ */ jsxs(
2112
+ "select",
2113
+ {
2114
+ "aria-label": L.bulkSetPriority,
2115
+ disabled: applying,
2116
+ value: "",
2117
+ onChange: (e) => {
2118
+ const v = e.target.value;
2119
+ if (v) void applyBulk({ priority: v });
2120
+ },
2121
+ className: bulkFieldClass,
2122
+ children: [
2123
+ /* @__PURE__ */ jsx("option", { value: "", children: L.bulkSetPriority }),
2124
+ TASK_PRIORITIES.map((p) => /* @__PURE__ */ jsx("option", { value: p, children: L.priority[p] }, p))
2125
+ ]
2126
+ }
2127
+ ),
2128
+ assigneeOptions && assigneeOptions.length > 0 && /* @__PURE__ */ jsxs(
2129
+ "select",
2130
+ {
2131
+ "aria-label": L.bulkAssign,
2132
+ disabled: applying,
2133
+ value: "",
2134
+ onChange: (e) => {
2135
+ const v = e.target.value;
2136
+ if (v) void applyBulk({ assigneeId: v });
2137
+ },
2138
+ className: bulkFieldClass,
2139
+ children: [
2140
+ /* @__PURE__ */ jsx("option", { value: "", children: L.bulkAssign }),
2141
+ assigneeOptions.map((o) => /* @__PURE__ */ jsx("option", { value: o.id, children: o.name }, o.id))
2142
+ ]
2143
+ }
2144
+ ),
2145
+ /* @__PURE__ */ jsxs(
2146
+ "button",
2147
+ {
2148
+ type: "button",
2149
+ disabled: applying,
2150
+ onClick: () => void applyBulkDelete(),
2151
+ className: "inline-flex items-center gap-1 rounded-md px-2 py-1 text-destructive hover:bg-destructive/10 disabled:opacity-50",
2152
+ children: [
2153
+ /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
2154
+ L.bulkDelete
2155
+ ]
2156
+ }
2157
+ ),
2158
+ /* @__PURE__ */ jsx(
2159
+ "button",
2160
+ {
2161
+ type: "button",
2162
+ disabled: applying,
2163
+ onClick: clearSelection,
2164
+ className: "rounded-md px-2 py-1 text-muted-foreground hover:bg-muted disabled:opacity-50",
2165
+ children: L.bulkClear
2166
+ }
2167
+ ),
2168
+ applying && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: L.bulkApplying }),
2169
+ bulkError && /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive", children: bulkError })
2170
+ ] }),
1844
2171
  /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded-lg border border-border", children: /* @__PURE__ */ jsxs("table", { className: "w-full border-collapse text-sm", children: [
1845
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { className: "border-b border-border bg-muted/50 text-left", children: columns.map((col) => {
1846
- const sortable = Boolean(COLUMN_SORT[col]);
1847
- const active = sort.by === COLUMN_SORT[col];
1848
- return /* @__PURE__ */ jsx(
1849
- "th",
2172
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b border-border bg-muted/50 text-left", children: [
2173
+ selectable && /* @__PURE__ */ jsx("th", { className: "w-9 px-3 py-2", children: /* @__PURE__ */ jsx(
2174
+ BulkCheckbox,
1850
2175
  {
1851
- onClick: sortable ? () => toggleSort(col) : void 0,
1852
- className: cn(
1853
- "px-3 py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
1854
- sortable && "cursor-pointer select-none hover:text-foreground"
1855
- ),
1856
- children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
1857
- columnLabel(col, L),
1858
- active && (sort.dir === "asc" ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-3 w-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3", "aria-hidden": "true" }))
1859
- ] })
1860
- },
1861
- col
1862
- );
1863
- }) }) }),
2176
+ checked: allSelected,
2177
+ indeterminate: someSelected,
2178
+ disabled: rows.length === 0 || applying,
2179
+ "aria-label": L.bulkSelected.replace("{n}", "all"),
2180
+ onChange: toggleAll
2181
+ }
2182
+ ) }),
2183
+ columns.map((col) => {
2184
+ const sortable = Boolean(COLUMN_SORT[col]);
2185
+ const active = sort.by === COLUMN_SORT[col];
2186
+ return /* @__PURE__ */ jsx(
2187
+ "th",
2188
+ {
2189
+ onClick: sortable ? () => toggleSort(col) : void 0,
2190
+ className: cn(
2191
+ "px-3 py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
2192
+ sortable && "cursor-pointer select-none hover:text-foreground"
2193
+ ),
2194
+ children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", children: [
2195
+ columnLabel(col, L),
2196
+ active && (sort.dir === "asc" ? /* @__PURE__ */ jsx(ChevronUp, { className: "h-3 w-3", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3", "aria-hidden": "true" }))
2197
+ ] })
2198
+ },
2199
+ col
2200
+ );
2201
+ })
2202
+ ] }) }),
1864
2203
  /* @__PURE__ */ jsxs("tbody", { children: [
1865
2204
  rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
1866
2205
  "td",
1867
2206
  {
1868
- colSpan: columns.length,
2207
+ colSpan: colCount,
1869
2208
  className: "px-3 py-8 text-center text-muted-foreground",
1870
2209
  children: L.noTasksFound
1871
2210
  }
1872
2211
  ) }),
1873
- rows.map((task) => /* @__PURE__ */ jsx(
2212
+ rows.map((task) => /* @__PURE__ */ jsxs(
1874
2213
  "tr",
1875
2214
  {
1876
2215
  onClick: onSelect ? () => onSelect(task.id) : void 0,
1877
2216
  className: cn(
1878
2217
  "border-b border-border last:border-0",
1879
- onSelect && "cursor-pointer hover:bg-muted/40"
2218
+ onSelect && "cursor-pointer hover:bg-muted/40",
2219
+ selectable && selected.has(task.id) && "bg-primary/5"
1880
2220
  ),
1881
- children: columns.map((col) => /* @__PURE__ */ jsx(
1882
- "td",
1883
- {
1884
- className: "px-3 py-2 align-middle text-foreground",
1885
- children: renderCell(col, task, {
1886
- resolveAssignee,
1887
- links: linksByItem.get(task.id),
1888
- renderLinkIcon,
1889
- unassigned: L.unassigned
1890
- })
1891
- },
1892
- col
1893
- ))
2221
+ children: [
2222
+ selectable && /* @__PURE__ */ jsx("td", { className: "w-9 px-3 py-2 align-middle", children: /* @__PURE__ */ jsx(
2223
+ BulkCheckbox,
2224
+ {
2225
+ checked: selected.has(task.id),
2226
+ disabled: applying,
2227
+ "aria-label": task.title,
2228
+ onClick: (e) => e.stopPropagation(),
2229
+ onChange: () => toggleRow(task.id)
2230
+ }
2231
+ ) }),
2232
+ columns.map((col) => /* @__PURE__ */ jsx(
2233
+ "td",
2234
+ {
2235
+ className: "px-3 py-2 align-middle text-foreground",
2236
+ children: renderCell(col, task, {
2237
+ resolveAssignee,
2238
+ links: linksByItem.get(task.id),
2239
+ renderLinkIcon,
2240
+ unassigned: L.unassigned
2241
+ })
2242
+ },
2243
+ col
2244
+ ))
2245
+ ]
1894
2246
  },
1895
2247
  task.id
1896
2248
  ))
@@ -2071,6 +2423,19 @@ function useMilestones(workspaceId, opts = {}) {
2071
2423
  }, [milestones]);
2072
2424
  return { milestones, byDay, loading, creating, createMilestone };
2073
2425
  }
2426
+ var LOCALE_MAP = {
2427
+ en: enUS,
2428
+ hi,
2429
+ zh: zhCN,
2430
+ es,
2431
+ ar,
2432
+ ta
2433
+ };
2434
+ function resolveDateLocale(lang) {
2435
+ if (!lang) return enUS;
2436
+ const primary = lang.toLowerCase().split(/[-_]/)[0];
2437
+ return LOCALE_MAP[primary] ?? enUS;
2438
+ }
2074
2439
  var WEEK_OPTS = { weekStartsOn: 1 };
2075
2440
  function dayKey(d) {
2076
2441
  return format(d, "yyyy-MM-dd");
@@ -2087,12 +2452,21 @@ function TaskTimelineCore({
2087
2452
  showMilestones = true,
2088
2453
  canCreateMilestone = false,
2089
2454
  milestoneProjectId,
2455
+ locale,
2090
2456
  className,
2091
2457
  workspaceId: workspaceIdProp
2092
2458
  }) {
2093
2459
  const ctx = usePlanMagnetContextSafe();
2094
2460
  const workspaceId = workspaceIdProp ?? ctx?.workspaceId;
2095
2461
  const L = useTaskLabels();
2462
+ const dfLocale = resolveDateLocale(locale);
2463
+ const weekdayHeaders = useMemo(() => {
2464
+ const weekStart = startOfWeek(new Date(2026, 0, 5), WEEK_OPTS);
2465
+ return Array.from(
2466
+ { length: 7 },
2467
+ (_, i) => format(addDays(weekStart, i), "EEE", { locale: dfLocale })
2468
+ );
2469
+ }, [dfLocale]);
2096
2470
  const {
2097
2471
  milestones,
2098
2472
  byDay: milestonesByDay,
@@ -2210,7 +2584,7 @@ function TaskTimelineCore({
2210
2584
  error.message
2211
2585
  ] });
2212
2586
  }
2213
- const rangeLabel = mode === "week" ? `${format(rangeStart, "MMM d")} \u2013 ${format(rangeEnd, "MMM d")}` : format(anchor, "MMMM yyyy");
2587
+ const rangeLabel = mode === "week" ? `${format(rangeStart, "MMM d", { locale: dfLocale })} \u2013 ${format(rangeEnd, "MMM d", { locale: dfLocale })}` : format(anchor, "MMMM yyyy", { locale: dfLocale });
2214
2588
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-3", className), children: [
2215
2589
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
2216
2590
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -2372,7 +2746,7 @@ function TaskTimelineCore({
2372
2746
  "text-xs font-semibold uppercase",
2373
2747
  today ? "text-primary" : "text-muted-foreground"
2374
2748
  ),
2375
- children: format(day, "EEE")
2749
+ children: format(day, "EEE", { locale: dfLocale })
2376
2750
  }
2377
2751
  ),
2378
2752
  /* @__PURE__ */ jsx(
@@ -2382,7 +2756,7 @@ function TaskTimelineCore({
2382
2756
  "text-sm font-semibold",
2383
2757
  today ? "text-primary" : "text-foreground"
2384
2758
  ),
2385
- children: format(day, "d")
2759
+ children: format(day, "d", { locale: dfLocale })
2386
2760
  }
2387
2761
  )
2388
2762
  ] }),
@@ -2403,13 +2777,13 @@ function TaskTimelineCore({
2403
2777
  dayKey(day)
2404
2778
  );
2405
2779
  }) }) : /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-7 gap-1", children: [
2406
- ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((d) => /* @__PURE__ */ jsx(
2780
+ weekdayHeaders.map((d, i) => /* @__PURE__ */ jsx(
2407
2781
  "div",
2408
2782
  {
2409
2783
  className: "px-1 pb-1 text-center text-[10px] font-semibold uppercase text-muted-foreground",
2410
2784
  children: d
2411
2785
  },
2412
- d
2786
+ i
2413
2787
  )),
2414
2788
  days.map((day) => {
2415
2789
  const items = byDay.get(dayKey(day)) ?? [];
@@ -2431,7 +2805,7 @@ function TaskTimelineCore({
2431
2805
  "text-[10px] font-semibold",
2432
2806
  today ? "text-primary" : "text-muted-foreground"
2433
2807
  ),
2434
- children: format(day, "d")
2808
+ children: format(day, "d", { locale: dfLocale })
2435
2809
  }
2436
2810
  ),
2437
2811
  items.slice(0, 3).map((t) => /* @__PURE__ */ jsx(
@@ -2535,6 +2909,7 @@ function TaskToolbar({
2535
2909
  views = ["board", "list", "timeline"],
2536
2910
  search,
2537
2911
  onSearchChange,
2912
+ searchInputRef,
2538
2913
  priority,
2539
2914
  onPriorityChange,
2540
2915
  type,
@@ -2543,6 +2918,7 @@ function TaskToolbar({
2543
2918
  canCreate,
2544
2919
  mine,
2545
2920
  onMineChange,
2921
+ leadingActions,
2546
2922
  className
2547
2923
  }) {
2548
2924
  const L = useTaskLabels();
@@ -2591,6 +2967,7 @@ function TaskToolbar({
2591
2967
  }
2592
2968
  ),
2593
2969
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2970
+ leadingActions,
2594
2971
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2595
2972
  /* @__PURE__ */ jsx(
2596
2973
  Search,
@@ -2602,6 +2979,7 @@ function TaskToolbar({
2602
2979
  /* @__PURE__ */ jsx(
2603
2980
  "input",
2604
2981
  {
2982
+ ref: searchInputRef,
2605
2983
  type: "search",
2606
2984
  value: search,
2607
2985
  onChange: (e) => onSearchChange(e.target.value),
@@ -2774,6 +3152,7 @@ function CommentComposer({
2774
3152
  ) })
2775
3153
  ] });
2776
3154
  }
3155
+ var REACTION_EMOJIS = ["\u{1F44D}", "\u2764\uFE0F", "\u{1F389}", "\u{1F440}"];
2777
3156
  var labelClass2 = "text-xs font-medium uppercase tracking-wide text-muted-foreground";
2778
3157
  var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2779
3158
  function TaskCommentsCore({
@@ -2789,6 +3168,11 @@ function TaskCommentsCore({
2789
3168
  const L = useTaskLabels();
2790
3169
  const [error, setError] = useState(null);
2791
3170
  const [replyingTo, setReplyingTo] = useState(null);
3171
+ const [editingId, setEditingId] = useState(null);
3172
+ const [editText, setEditText] = useState("");
3173
+ const [savingEdit, setSavingEdit] = useState(false);
3174
+ const [pickerOpenId, setPickerOpenId] = useState(null);
3175
+ const [reactingId, setReactingId] = useState(null);
2792
3176
  const { data, loading, refetch } = useQuery(
2793
3177
  GET_ENTITY_COMMENTS,
2794
3178
  {
@@ -2831,6 +3215,13 @@ function TaskCommentsCore({
2831
3215
  const [deleteComment] = useMutation(
2832
3216
  DELETE_ENTITY_COMMENT
2833
3217
  );
3218
+ const [updateComment] = useMutation(
3219
+ UPDATE_ENTITY_COMMENT
3220
+ );
3221
+ const [addReaction] = useMutation(
3222
+ ADD_ENTITY_COMMENT_REACTION
3223
+ );
3224
+ const [removeReaction] = useMutation(REMOVE_ENTITY_COMMENT_REACTION);
2834
3225
  const authorName = (id) => id === currentActorId ? L.you : resolveAssignee?.(id)?.name ?? void 0;
2835
3226
  const post = async ({ text, mentionUserIds }, parentCommentId) => {
2836
3227
  setError(null);
@@ -2871,6 +3262,57 @@ function TaskCommentsCore({
2871
3262
  setError(L.failedToDeleteComment);
2872
3263
  }
2873
3264
  };
3265
+ const startEdit = (c) => {
3266
+ setError(null);
3267
+ setEditingId(c.id);
3268
+ setEditText(c.content?.text ?? "");
3269
+ };
3270
+ const cancelEdit = () => {
3271
+ setEditingId(null);
3272
+ setEditText("");
3273
+ };
3274
+ const saveEdit = async (id) => {
3275
+ const text = editText.trim();
3276
+ if (!text || savingEdit) return;
3277
+ setError(null);
3278
+ setSavingEdit(true);
3279
+ try {
3280
+ await updateComment({
3281
+ variables: { input: { id, content: { text }, contentType: "TEXT" } }
3282
+ });
3283
+ await refetch();
3284
+ setEditingId(null);
3285
+ setEditText("");
3286
+ } catch {
3287
+ setError(L.failedToEditComment);
3288
+ } finally {
3289
+ setSavingEdit(false);
3290
+ }
3291
+ };
3292
+ const hasReacted = (c, emoji) => {
3293
+ if (!currentActorId) return false;
3294
+ const row = (c.reactionSummary ?? []).find((r) => r.emoji === emoji);
3295
+ return !!row?.userIds.includes(currentActorId);
3296
+ };
3297
+ const toggleReaction = async (c, emoji) => {
3298
+ if (!currentActorId || reactingId === c.id) return;
3299
+ setError(null);
3300
+ setPickerOpenId(null);
3301
+ setReactingId(c.id);
3302
+ const input = { commentId: c.id, emoji, userId: currentActorId };
3303
+ try {
3304
+ if (hasReacted(c, emoji)) {
3305
+ await removeReaction({ variables: { input } });
3306
+ } else {
3307
+ await addReaction({ variables: { input } });
3308
+ }
3309
+ await refetch();
3310
+ } catch {
3311
+ setError(L.failedToReact);
3312
+ } finally {
3313
+ setReactingId(null);
3314
+ }
3315
+ };
2874
3316
  const renderBody = (c) => {
2875
3317
  const text = c.content?.text ?? "";
2876
3318
  const names = (members ?? []).map((m) => m.name).filter((n) => text.includes(`@${n}`));
@@ -2888,37 +3330,150 @@ function TaskCommentsCore({
2888
3330
  ) : /* @__PURE__ */ jsx("span", { children: part }, i)
2889
3331
  );
2890
3332
  };
2891
- const commentBlock = (c, isReply) => /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2892
- /* @__PURE__ */ jsx(Avatar, { id: c.authorId, name: authorName(c.authorId), size: "sm" }),
2893
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
2894
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2895
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: authorName(c.authorId) ?? c.authorId.slice(0, 8) }),
2896
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatDateFull(c.createdAt) }),
2897
- c.edited && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: L.editedTag }),
2898
- !readonly && c.authorId === currentActorId && /* @__PURE__ */ jsx(
3333
+ const reactionBar = (c) => {
3334
+ const summary = (c.reactionSummary ?? []).filter((r) => r.count > 0);
3335
+ const canReact = !readonly && !!currentActorId;
3336
+ if (summary.length === 0 && !canReact) return null;
3337
+ return /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-1", children: [
3338
+ summary.map((r) => {
3339
+ const mine = hasReacted(c, r.emoji);
3340
+ return /* @__PURE__ */ jsxs(
2899
3341
  "button",
2900
3342
  {
2901
3343
  type: "button",
2902
- onClick: () => handleDelete(c.id),
2903
- "aria-label": L.del,
2904
- title: L.del,
2905
- className: "ml-auto rounded p-1 text-muted-foreground hover:text-destructive",
2906
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
3344
+ disabled: !canReact || reactingId === c.id,
3345
+ onClick: () => toggleReaction(c, r.emoji),
3346
+ "aria-label": `${r.emoji} ${r.count}`,
3347
+ "aria-pressed": mine,
3348
+ className: cn(
3349
+ "inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-xs",
3350
+ mine ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground",
3351
+ canReact ? "hover:border-primary" : "cursor-default"
3352
+ ),
3353
+ children: [
3354
+ /* @__PURE__ */ jsx("span", { children: r.emoji }),
3355
+ /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: r.count })
3356
+ ]
3357
+ },
3358
+ r.emoji
3359
+ );
3360
+ }),
3361
+ canReact && /* @__PURE__ */ jsxs("div", { className: "relative", children: [
3362
+ /* @__PURE__ */ jsx(
3363
+ "button",
3364
+ {
3365
+ type: "button",
3366
+ onClick: () => setPickerOpenId((prev) => prev === c.id ? null : c.id),
3367
+ "aria-label": L.addReaction,
3368
+ "aria-expanded": pickerOpenId === c.id,
3369
+ title: L.addReaction,
3370
+ className: "inline-flex items-center rounded-full border border-border p-1 text-muted-foreground hover:border-primary hover:text-primary",
3371
+ children: /* @__PURE__ */ jsx(SmilePlus, { className: "h-3.5 w-3.5" })
3372
+ }
3373
+ ),
3374
+ pickerOpenId === c.id && /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-full z-20 mt-1 flex gap-1 rounded-md border border-border bg-popover p-1 shadow-md", children: REACTION_EMOJIS.map((emoji) => /* @__PURE__ */ jsx(
3375
+ "button",
3376
+ {
3377
+ type: "button",
3378
+ disabled: reactingId === c.id,
3379
+ onClick: () => toggleReaction(c, emoji),
3380
+ "aria-label": emoji,
3381
+ className: cn(
3382
+ "rounded px-1 py-0.5 text-sm hover:bg-muted",
3383
+ hasReacted(c, emoji) && "bg-primary/10"
3384
+ ),
3385
+ children: emoji
3386
+ },
3387
+ emoji
3388
+ )) })
3389
+ ] })
3390
+ ] });
3391
+ };
3392
+ const commentBlock = (c, isReply) => {
3393
+ const isMine = c.authorId === currentActorId;
3394
+ const isEditing = editingId === c.id;
3395
+ return /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3396
+ /* @__PURE__ */ jsx(Avatar, { id: c.authorId, name: authorName(c.authorId), size: "sm" }),
3397
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
3398
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3399
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: authorName(c.authorId) ?? c.authorId.slice(0, 8) }),
3400
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatDateFull(c.createdAt) }),
3401
+ c.edited && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: L.editedTag }),
3402
+ !readonly && isMine && !isEditing && /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
3403
+ /* @__PURE__ */ jsx(
3404
+ "button",
3405
+ {
3406
+ type: "button",
3407
+ onClick: () => startEdit(c),
3408
+ "aria-label": L.editComment,
3409
+ title: L.editComment,
3410
+ className: "rounded p-1 text-muted-foreground hover:text-foreground",
3411
+ children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
3412
+ }
3413
+ ),
3414
+ /* @__PURE__ */ jsx(
3415
+ "button",
3416
+ {
3417
+ type: "button",
3418
+ onClick: () => handleDelete(c.id),
3419
+ "aria-label": L.del,
3420
+ title: L.del,
3421
+ className: "rounded p-1 text-muted-foreground hover:text-destructive",
3422
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
3423
+ }
3424
+ )
3425
+ ] })
3426
+ ] }),
3427
+ isEditing ? /* @__PURE__ */ jsxs("div", { className: "mt-1 space-y-2", children: [
3428
+ /* @__PURE__ */ jsx(
3429
+ "textarea",
3430
+ {
3431
+ className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring",
3432
+ rows: 2,
3433
+ value: editText,
3434
+ autoFocus: true,
3435
+ "aria-label": L.editComment,
3436
+ onChange: (e) => setEditText(e.target.value)
3437
+ }
3438
+ ),
3439
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
3440
+ /* @__PURE__ */ jsx(
3441
+ "button",
3442
+ {
3443
+ type: "button",
3444
+ onClick: cancelEdit,
3445
+ className: "rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground hover:text-foreground",
3446
+ children: L.cancel
3447
+ }
3448
+ ),
3449
+ /* @__PURE__ */ jsx(
3450
+ "button",
3451
+ {
3452
+ type: "button",
3453
+ onClick: () => saveEdit(c.id),
3454
+ disabled: savingEdit || !editText.trim(),
3455
+ className: cn(
3456
+ "rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground",
3457
+ (savingEdit || !editText.trim()) && "cursor-not-allowed opacity-50"
3458
+ ),
3459
+ children: savingEdit ? L.saving : L.saveComment
3460
+ }
3461
+ )
3462
+ ] })
3463
+ ] }) : /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: renderBody(c) }),
3464
+ !isEditing && reactionBar(c),
3465
+ !isReply && !readonly && !isEditing && /* @__PURE__ */ jsx(
3466
+ "button",
3467
+ {
3468
+ type: "button",
3469
+ onClick: () => setReplyingTo((prev) => prev === c.id ? null : c.id),
3470
+ className: "mt-1 text-xs font-medium text-muted-foreground hover:text-foreground",
3471
+ children: L.reply
2907
3472
  }
2908
3473
  )
2909
- ] }),
2910
- /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap break-words text-sm text-foreground", children: renderBody(c) }),
2911
- !isReply && !readonly && /* @__PURE__ */ jsx(
2912
- "button",
2913
- {
2914
- type: "button",
2915
- onClick: () => setReplyingTo((prev) => prev === c.id ? null : c.id),
2916
- className: "mt-1 text-xs font-medium text-muted-foreground hover:text-foreground",
2917
- children: L.reply
2918
- }
2919
- )
2920
- ] })
2921
- ] });
3474
+ ] })
3475
+ ] });
3476
+ };
2922
3477
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), children: [
2923
3478
  /* @__PURE__ */ jsxs("div", { className: labelClass2, children: [
2924
3479
  L.commentsLabel,
@@ -3483,6 +4038,300 @@ function TaskDetail({
3483
4038
  }
3484
4039
  );
3485
4040
  }
4041
+ function SavedViewsMenu({
4042
+ views,
4043
+ activeViewId,
4044
+ onSaveView,
4045
+ onApplyView,
4046
+ onDeleteView,
4047
+ className
4048
+ }) {
4049
+ const L = useTaskLabels();
4050
+ const [open, setOpen] = useState(false);
4051
+ const [naming, setNaming] = useState(false);
4052
+ const [name, setName] = useState("");
4053
+ const rootRef = useRef(null);
4054
+ useEffect(() => {
4055
+ if (!open) return;
4056
+ const onDown = (e) => {
4057
+ if (rootRef.current && !rootRef.current.contains(e.target)) {
4058
+ setOpen(false);
4059
+ setNaming(false);
4060
+ }
4061
+ };
4062
+ const onKey = (e) => {
4063
+ if (e.key === "Escape") {
4064
+ setOpen(false);
4065
+ setNaming(false);
4066
+ }
4067
+ };
4068
+ document.addEventListener("mousedown", onDown);
4069
+ document.addEventListener("keydown", onKey);
4070
+ return () => {
4071
+ document.removeEventListener("mousedown", onDown);
4072
+ document.removeEventListener("keydown", onKey);
4073
+ };
4074
+ }, [open]);
4075
+ const activeName = views.find((v) => v.id === activeViewId)?.name ?? L.defaultViewName;
4076
+ const commitSave = () => {
4077
+ const trimmed = name.trim();
4078
+ if (!trimmed) return;
4079
+ onSaveView(trimmed);
4080
+ setName("");
4081
+ setNaming(false);
4082
+ setOpen(false);
4083
+ };
4084
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative", className), children: [
4085
+ /* @__PURE__ */ jsxs(
4086
+ "button",
4087
+ {
4088
+ type: "button",
4089
+ onClick: () => setOpen((o) => !o),
4090
+ "aria-haspopup": "menu",
4091
+ "aria-expanded": open,
4092
+ "aria-label": L.savedViews,
4093
+ className: cn(
4094
+ "flex h-8 max-w-[10rem] items-center gap-1.5 rounded-md border px-2.5 text-xs font-medium transition-colors",
4095
+ activeViewId ? "border-primary bg-primary/10 text-primary" : "border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4096
+ ),
4097
+ children: [
4098
+ /* @__PURE__ */ jsx(Bookmark, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }),
4099
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: activeName })
4100
+ ]
4101
+ }
4102
+ ),
4103
+ open && /* @__PURE__ */ jsxs(
4104
+ "div",
4105
+ {
4106
+ role: "menu",
4107
+ className: "absolute right-0 top-full z-30 mt-1 w-56 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
4108
+ children: [
4109
+ /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-[0.65rem] font-semibold uppercase tracking-wide text-muted-foreground", children: L.savedViews }),
4110
+ views.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: L.noSavedViews }) : /* @__PURE__ */ jsx("ul", { className: "max-h-56 overflow-auto", children: views.map((v) => {
4111
+ const active = v.id === activeViewId;
4112
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
4113
+ /* @__PURE__ */ jsxs(
4114
+ "button",
4115
+ {
4116
+ type: "button",
4117
+ role: "menuitemradio",
4118
+ "aria-checked": active,
4119
+ onClick: () => {
4120
+ onApplyView(v.id);
4121
+ setOpen(false);
4122
+ },
4123
+ className: cn(
4124
+ "flex min-w-0 flex-1 items-center gap-1.5 rounded px-2 py-1.5 text-left text-xs transition-colors hover:bg-muted/60",
4125
+ active ? "text-foreground" : "text-muted-foreground"
4126
+ ),
4127
+ children: [
4128
+ /* @__PURE__ */ jsx(
4129
+ Check,
4130
+ {
4131
+ className: cn(
4132
+ "h-3.5 w-3.5 shrink-0",
4133
+ active ? "opacity-100" : "opacity-0"
4134
+ ),
4135
+ "aria-hidden": "true"
4136
+ }
4137
+ ),
4138
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: v.name })
4139
+ ]
4140
+ }
4141
+ ),
4142
+ /* @__PURE__ */ jsx(
4143
+ "button",
4144
+ {
4145
+ type: "button",
4146
+ onClick: () => onDeleteView(v.id),
4147
+ "aria-label": `${L.deleteView}: ${v.name}`,
4148
+ className: "shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
4149
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
4150
+ }
4151
+ )
4152
+ ] }, v.id);
4153
+ }) }),
4154
+ /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-border" }),
4155
+ naming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 p-1", children: [
4156
+ /* @__PURE__ */ jsx(
4157
+ "input",
4158
+ {
4159
+ type: "text",
4160
+ autoFocus: true,
4161
+ value: name,
4162
+ onChange: (e) => setName(e.target.value),
4163
+ onKeyDown: (e) => {
4164
+ if (e.key === "Enter") commitSave();
4165
+ },
4166
+ placeholder: L.viewName,
4167
+ "aria-label": L.viewName,
4168
+ className: "h-7 min-w-0 flex-1 rounded border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
4169
+ }
4170
+ ),
4171
+ /* @__PURE__ */ jsx(
4172
+ "button",
4173
+ {
4174
+ type: "button",
4175
+ onClick: commitSave,
4176
+ disabled: !name.trim(),
4177
+ className: "h-7 shrink-0 rounded bg-primary px-2 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50",
4178
+ children: L.saveViewAction
4179
+ }
4180
+ )
4181
+ ] }) : /* @__PURE__ */ jsxs(
4182
+ "button",
4183
+ {
4184
+ type: "button",
4185
+ role: "menuitem",
4186
+ onClick: () => setNaming(true),
4187
+ className: "flex w-full items-center gap-1.5 rounded px-2 py-1.5 text-left text-xs font-medium text-foreground transition-colors hover:bg-muted/60",
4188
+ children: [
4189
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
4190
+ L.saveView
4191
+ ]
4192
+ }
4193
+ )
4194
+ ]
4195
+ }
4196
+ )
4197
+ ] });
4198
+ }
4199
+ var STORAGE_PREFIX = "planmagnet:views:";
4200
+ var DEBOUNCE_MS = 400;
4201
+ function makeStorageKey(workspaceId, suffix) {
4202
+ return suffix ? `${STORAGE_PREFIX}${workspaceId}:${suffix}` : `${STORAGE_PREFIX}${workspaceId}`;
4203
+ }
4204
+ function safeStorage() {
4205
+ try {
4206
+ if (typeof window === "undefined") return null;
4207
+ const ls = window.localStorage;
4208
+ const probe = "__pm_probe__";
4209
+ ls.setItem(probe, probe);
4210
+ ls.removeItem(probe);
4211
+ return ls;
4212
+ } catch {
4213
+ return null;
4214
+ }
4215
+ }
4216
+ function readPayload(key) {
4217
+ const ls = safeStorage();
4218
+ if (!ls) return null;
4219
+ try {
4220
+ const raw = ls.getItem(key);
4221
+ if (!raw) return null;
4222
+ const parsed = JSON.parse(raw);
4223
+ if (!parsed || parsed.v !== 1) return null;
4224
+ return parsed;
4225
+ } catch {
4226
+ return null;
4227
+ }
4228
+ }
4229
+ function writePayload(key, payload) {
4230
+ const ls = safeStorage();
4231
+ if (!ls) return;
4232
+ try {
4233
+ ls.setItem(key, JSON.stringify(payload));
4234
+ } catch {
4235
+ }
4236
+ }
4237
+ function makeId() {
4238
+ const rand = Math.random().toString(36).slice(2, 10);
4239
+ return `v_${Date.now().toString(36)}_${rand}`;
4240
+ }
4241
+ function useSavedViews({
4242
+ storageKey,
4243
+ enabled,
4244
+ current
4245
+ }) {
4246
+ const initial = useMemo(
4247
+ () => enabled ? readPayload(storageKey) : null,
4248
+ [enabled, storageKey]
4249
+ );
4250
+ const [views, setViews] = useState(initial?.views ?? []);
4251
+ const [activeViewId, setActiveViewId] = useState(
4252
+ initial?.activeViewId ?? null
4253
+ );
4254
+ const restored = enabled ? initial?.current ?? null : null;
4255
+ const viewsRef = useRef(views);
4256
+ viewsRef.current = views;
4257
+ const activeViewIdRef = useRef(activeViewId);
4258
+ activeViewIdRef.current = activeViewId;
4259
+ useEffect(() => {
4260
+ if (!enabled) return;
4261
+ const handle = setTimeout(() => {
4262
+ writePayload(storageKey, {
4263
+ v: 1,
4264
+ current,
4265
+ views: viewsRef.current,
4266
+ activeViewId: activeViewIdRef.current
4267
+ });
4268
+ }, DEBOUNCE_MS);
4269
+ return () => clearTimeout(handle);
4270
+ }, [enabled, storageKey, current]);
4271
+ const flush = useCallback(
4272
+ (nextViews, nextActive) => {
4273
+ if (!enabled) return;
4274
+ writePayload(storageKey, {
4275
+ v: 1,
4276
+ current,
4277
+ views: nextViews,
4278
+ activeViewId: nextActive
4279
+ });
4280
+ },
4281
+ [enabled, storageKey, current]
4282
+ );
4283
+ const saveView = useCallback(
4284
+ (name) => {
4285
+ if (!enabled) return null;
4286
+ const trimmed = name.trim();
4287
+ if (!trimmed) return null;
4288
+ const id = makeId();
4289
+ const next = [
4290
+ ...viewsRef.current,
4291
+ { id, name: trimmed, state: current }
4292
+ ];
4293
+ setViews(next);
4294
+ setActiveViewId(id);
4295
+ flush(next, id);
4296
+ return id;
4297
+ },
4298
+ [enabled, current, flush]
4299
+ );
4300
+ const applyView = useCallback(
4301
+ (id) => {
4302
+ const found = viewsRef.current.find((v) => v.id === id);
4303
+ if (!found) return null;
4304
+ setActiveViewId(id);
4305
+ flush(viewsRef.current, id);
4306
+ return found.state;
4307
+ },
4308
+ [flush]
4309
+ );
4310
+ const deleteView = useCallback(
4311
+ (id) => {
4312
+ const next = viewsRef.current.filter((v) => v.id !== id);
4313
+ const nextActive = activeViewIdRef.current === id ? null : activeViewIdRef.current;
4314
+ setViews(next);
4315
+ setActiveViewId(nextActive);
4316
+ flush(next, nextActive);
4317
+ },
4318
+ [flush]
4319
+ );
4320
+ const clearActiveView = useCallback(() => {
4321
+ if (activeViewIdRef.current == null) return;
4322
+ setActiveViewId(null);
4323
+ flush(viewsRef.current, null);
4324
+ }, [flush]);
4325
+ return {
4326
+ restored,
4327
+ views,
4328
+ activeViewId,
4329
+ saveView,
4330
+ applyView,
4331
+ deleteView,
4332
+ clearActiveView
4333
+ };
4334
+ }
3486
4335
  function TasksPageCore({
3487
4336
  workspaceId: workspaceIdProp,
3488
4337
  projectId,
@@ -3490,6 +4339,7 @@ function TasksPageCore({
3490
4339
  columns,
3491
4340
  readonly,
3492
4341
  canCreateMilestone,
4342
+ locale,
3493
4343
  hiddenFields,
3494
4344
  views,
3495
4345
  defaultView = "board",
@@ -3507,19 +4357,26 @@ function TasksPageCore({
3507
4357
  onTaskSaved,
3508
4358
  createSurface = "modal",
3509
4359
  detailSurface = "drawer",
4360
+ enableSavedViews,
4361
+ persistViewKey,
4362
+ enableShortcuts,
3510
4363
  className
3511
4364
  }) {
3512
4365
  const ctx = usePlanMagnetContextSafe();
3513
4366
  const workspaceId = workspaceIdProp ?? ctx?.workspaceId;
3514
4367
  const client = useApolloClient();
3515
4368
  const mergedLabels = mergeTaskLabels(labels);
4369
+ const savedViewsEnabled = Boolean(enableSavedViews || persistViewKey);
3516
4370
  const [view, setView] = useState(defaultView);
3517
4371
  const [search, setSearch] = useState("");
3518
4372
  const [priority, setPriority] = useState("ALL");
3519
4373
  const [type, setType] = useState("ALL");
3520
4374
  const [mine, setMine] = useState(false);
4375
+ const [savedListColumns, setSavedListColumns] = useState(null);
3521
4376
  const [creating, setCreating] = useState(false);
3522
4377
  const [selectedId, setSelectedId] = useState(null);
4378
+ const [showHelp, setShowHelp] = useState(false);
4379
+ const searchInputRef = useRef(null);
3523
4380
  const [createDefaults, setCreateDefaults] = useState(null);
3524
4381
  useEffect(() => {
3525
4382
  if (openItemId) setSelectedId(openItemId);
@@ -3571,6 +4428,169 @@ function TasksPageCore({
3571
4428
  setSelectedId(task.id);
3572
4429
  }, []);
3573
4430
  const handleSelect = useCallback((id) => setSelectedId(id), []);
4431
+ const persistEnabled = savedViewsEnabled && Boolean(workspaceId);
4432
+ const storageKey = useMemo(
4433
+ () => makeStorageKey(workspaceId ?? "", persistViewKey),
4434
+ [workspaceId, persistViewKey]
4435
+ );
4436
+ const currentViewState = useMemo(
4437
+ () => ({
4438
+ view,
4439
+ priority,
4440
+ type,
4441
+ search,
4442
+ mine,
4443
+ listColumns: savedListColumns
4444
+ }),
4445
+ [view, priority, type, search, mine, savedListColumns]
4446
+ );
4447
+ const applyViewState = useCallback((s) => {
4448
+ setView(s.view);
4449
+ setPriority(s.priority);
4450
+ setType(s.type);
4451
+ setSearch(s.search);
4452
+ setMine(s.mine);
4453
+ setSavedListColumns(s.listColumns);
4454
+ }, []);
4455
+ const {
4456
+ restored,
4457
+ views: savedViews,
4458
+ activeViewId,
4459
+ saveView,
4460
+ applyView,
4461
+ deleteView,
4462
+ clearActiveView
4463
+ } = useSavedViews({
4464
+ storageKey,
4465
+ enabled: persistEnabled,
4466
+ current: currentViewState
4467
+ });
4468
+ const [didRestore, setDidRestore] = useState(false);
4469
+ useEffect(() => {
4470
+ if (didRestore || !persistEnabled) return;
4471
+ if (restored) applyViewState(restored);
4472
+ setDidRestore(true);
4473
+ }, [persistEnabled]);
4474
+ const handleApplySavedView = useCallback(
4475
+ (id) => {
4476
+ const s = applyView(id);
4477
+ if (s) applyViewState(s);
4478
+ },
4479
+ [applyView, applyViewState]
4480
+ );
4481
+ const detachOnEdit = useCallback(() => {
4482
+ if (activeViewId != null) clearActiveView();
4483
+ }, [activeViewId, clearActiveView]);
4484
+ const handleViewChange = useCallback(
4485
+ (v) => {
4486
+ setView(v);
4487
+ detachOnEdit();
4488
+ },
4489
+ [detachOnEdit]
4490
+ );
4491
+ const handleSearchChange = useCallback(
4492
+ (s) => {
4493
+ setSearch(s);
4494
+ detachOnEdit();
4495
+ },
4496
+ [detachOnEdit]
4497
+ );
4498
+ const handlePriorityChange = useCallback(
4499
+ (p) => {
4500
+ setPriority(p);
4501
+ detachOnEdit();
4502
+ },
4503
+ [detachOnEdit]
4504
+ );
4505
+ const handleTypeChange = useCallback(
4506
+ (t) => {
4507
+ setType(t);
4508
+ detachOnEdit();
4509
+ },
4510
+ [detachOnEdit]
4511
+ );
4512
+ const handleMineChange = useCallback(
4513
+ (m) => {
4514
+ setMine(m);
4515
+ detachOnEdit();
4516
+ },
4517
+ [detachOnEdit]
4518
+ );
4519
+ const enabledViews = useMemo(
4520
+ () => views ?? ["board", "list", "timeline"],
4521
+ [views]
4522
+ );
4523
+ const shortcutsCanCreate = !readonly && Boolean(projectId);
4524
+ useEffect(() => {
4525
+ if (!enableShortcuts) return;
4526
+ const onKeyDown = (e) => {
4527
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
4528
+ const target = e.target;
4529
+ if (target) {
4530
+ const tag = target.tagName;
4531
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable) {
4532
+ if (e.key !== "Escape") return;
4533
+ }
4534
+ }
4535
+ switch (e.key) {
4536
+ case "c":
4537
+ if (shortcutsCanCreate) {
4538
+ e.preventDefault();
4539
+ startCreate();
4540
+ }
4541
+ break;
4542
+ case "/":
4543
+ e.preventDefault();
4544
+ searchInputRef.current?.focus();
4545
+ break;
4546
+ case "b":
4547
+ if (enabledViews.includes("board")) {
4548
+ e.preventDefault();
4549
+ setView("board");
4550
+ }
4551
+ break;
4552
+ case "l":
4553
+ if (enabledViews.includes("list")) {
4554
+ e.preventDefault();
4555
+ setView("list");
4556
+ }
4557
+ break;
4558
+ case "g":
4559
+ if (enabledViews.includes("timeline")) {
4560
+ e.preventDefault();
4561
+ setView("timeline");
4562
+ }
4563
+ break;
4564
+ case "m":
4565
+ if (currentActorId) {
4566
+ e.preventDefault();
4567
+ setMine((prev) => !prev);
4568
+ }
4569
+ break;
4570
+ case "?":
4571
+ e.preventDefault();
4572
+ setShowHelp((v) => !v);
4573
+ break;
4574
+ case "Escape":
4575
+ if (showHelp) setShowHelp(false);
4576
+ if (creating) cancelCreate();
4577
+ if (selectedId != null) setSelectedId(null);
4578
+ break;
4579
+ }
4580
+ };
4581
+ document.addEventListener("keydown", onKeyDown);
4582
+ return () => document.removeEventListener("keydown", onKeyDown);
4583
+ }, [
4584
+ enableShortcuts,
4585
+ enabledViews,
4586
+ shortcutsCanCreate,
4587
+ currentActorId,
4588
+ creating,
4589
+ selectedId,
4590
+ showHelp,
4591
+ startCreate,
4592
+ cancelCreate
4593
+ ]);
3574
4594
  if (!workspaceId) {
3575
4595
  return /* @__PURE__ */ jsx("div", { className: cn("p-6 text-sm text-destructive", className), children: "TasksPage: no workspaceId provided (prop or PlanMagnetProvider)." });
3576
4596
  }
@@ -3611,32 +4631,37 @@ function TasksPageCore({
3611
4631
  const listPriority = priority === "ALL" ? void 0 : priority;
3612
4632
  const listType = type === "ALL" ? void 0 : type;
3613
4633
  const mineAssignee = mine && currentActorId ? currentActorId : void 0;
3614
- const listColumns = linkSourceProduct ? [
3615
- "title",
3616
- "status",
3617
- "priority",
3618
- "assignee",
3619
- "dueDate",
3620
- "linkedRecord"
3621
- ] : void 0;
4634
+ const derivedListColumns = linkSourceProduct ? ["title", "status", "priority", "assignee", "dueDate", "linkedRecord"] : void 0;
4635
+ const listColumns = savedListColumns ?? derivedListColumns;
3622
4636
  return /* @__PURE__ */ jsx(TaskLabelsProvider, { value: mergedLabels, children: /* @__PURE__ */ jsxs("div", { className: cn("space-y-4 p-6", className), children: [
3623
4637
  header && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: typeof header === "string" ? /* @__PURE__ */ jsx("h1", { className: "truncate text-2xl font-semibold text-foreground", children: header }) : header }),
3624
4638
  /* @__PURE__ */ jsx(
3625
4639
  TaskToolbar,
3626
4640
  {
3627
4641
  view,
3628
- onViewChange: setView,
4642
+ onViewChange: savedViewsEnabled ? handleViewChange : setView,
3629
4643
  views,
3630
4644
  search,
3631
- onSearchChange: setSearch,
4645
+ onSearchChange: savedViewsEnabled ? handleSearchChange : setSearch,
4646
+ searchInputRef: enableShortcuts ? searchInputRef : void 0,
3632
4647
  priority,
3633
- onPriorityChange: setPriority,
4648
+ onPriorityChange: savedViewsEnabled ? handlePriorityChange : setPriority,
3634
4649
  type,
3635
- onTypeChange: setType,
4650
+ onTypeChange: savedViewsEnabled ? handleTypeChange : setType,
3636
4651
  onNewTask: startCreate,
3637
4652
  canCreate,
3638
4653
  mine,
3639
- onMineChange: currentActorId ? setMine : void 0
4654
+ onMineChange: currentActorId ? savedViewsEnabled ? handleMineChange : setMine : void 0,
4655
+ leadingActions: savedViewsEnabled ? /* @__PURE__ */ jsx(
4656
+ SavedViewsMenu,
4657
+ {
4658
+ views: savedViews,
4659
+ activeViewId,
4660
+ onSaveView: saveView,
4661
+ onApplyView: handleApplySavedView,
4662
+ onDeleteView: deleteView
4663
+ }
4664
+ ) : void 0
3640
4665
  }
3641
4666
  ),
3642
4667
  showInlineCreate && /* @__PURE__ */ jsx("div", { children: createForm }),
@@ -3670,6 +4695,8 @@ function TasksPageCore({
3670
4695
  search: search || void 0,
3671
4696
  assigneeId: mineAssignee,
3672
4697
  hideHeader: true,
4698
+ selectable: !readonly,
4699
+ assigneeOptions,
3673
4700
  resolveAssignee,
3674
4701
  linkSourceProduct,
3675
4702
  renderLinkIcon,
@@ -3689,7 +4716,8 @@ function TasksPageCore({
3689
4716
  resolveAssignee,
3690
4717
  onSelect: handleSelect,
3691
4718
  canCreateMilestone: canCreateMilestone ?? !readonly,
3692
- milestoneProjectId: boardProjectId ?? projectId
4719
+ milestoneProjectId: boardProjectId ?? projectId,
4720
+ locale
3693
4721
  }
3694
4722
  ),
3695
4723
  showInlineDetail && /* @__PURE__ */ jsx("div", { className: "pb-6", children: detailView }),
@@ -3711,9 +4739,60 @@ function TasksPageCore({
3711
4739
  ariaLabel: "Task detail",
3712
4740
  children: /* @__PURE__ */ jsx("div", { className: "p-2 pt-8", children: detailView })
3713
4741
  }
4742
+ ),
4743
+ enableShortcuts && /* @__PURE__ */ jsx(
4744
+ Modal,
4745
+ {
4746
+ open: showHelp,
4747
+ onClose: () => setShowHelp(false),
4748
+ ariaLabel: mergedLabels.keyboardShortcuts,
4749
+ className: "max-w-sm",
4750
+ children: /* @__PURE__ */ jsx(
4751
+ ShortcutsHelp,
4752
+ {
4753
+ labels: mergedLabels,
4754
+ enabledViews,
4755
+ canCreate,
4756
+ showMine: Boolean(currentActorId)
4757
+ }
4758
+ )
4759
+ }
3714
4760
  )
3715
4761
  ] }) });
3716
4762
  }
4763
+ function ShortcutRow({ keys, label }) {
4764
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4 py-1.5", children: [
4765
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-foreground", children: label }),
4766
+ /* @__PURE__ */ jsx("span", { className: "flex items-center gap-1", children: keys.map((k) => /* @__PURE__ */ jsx(
4767
+ "kbd",
4768
+ {
4769
+ className: "inline-flex min-w-[1.5rem] items-center justify-center rounded border border-border bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground",
4770
+ children: k
4771
+ },
4772
+ k
4773
+ )) })
4774
+ ] });
4775
+ }
4776
+ function ShortcutsHelp({
4777
+ labels: L,
4778
+ enabledViews,
4779
+ canCreate,
4780
+ showMine
4781
+ }) {
4782
+ return /* @__PURE__ */ jsxs("div", { className: "p-6 pt-8", children: [
4783
+ /* @__PURE__ */ jsx("h2", { className: "mb-3 text-base font-semibold text-foreground", children: L.keyboardShortcuts }),
4784
+ /* @__PURE__ */ jsxs("div", { className: "divide-y divide-border", children: [
4785
+ canCreate && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["c"], label: L.shortcutNewTask }),
4786
+ /* @__PURE__ */ jsx(ShortcutRow, { keys: ["/"], label: L.shortcutSearch }),
4787
+ enabledViews.includes("board") && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["b"], label: L.shortcutBoardView }),
4788
+ enabledViews.includes("list") && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["l"], label: L.shortcutListView }),
4789
+ enabledViews.includes("timeline") && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["g"], label: L.shortcutTimelineView }),
4790
+ showMine && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["m"], label: L.shortcutMyTasks }),
4791
+ /* @__PURE__ */ jsx(ShortcutRow, { keys: ["?"], label: L.shortcutHelp }),
4792
+ /* @__PURE__ */ jsx(ShortcutRow, { keys: ["Esc"], label: L.shortcutClose })
4793
+ ] })
4794
+ ] });
4795
+ }
3717
4796
  function TasksPage({
3718
4797
  apiGatewayUrl,
3719
4798
  authToken,
@@ -3743,6 +4822,6 @@ function TasksPage({
3743
4822
  );
3744
4823
  }
3745
4824
 
3746
- export { Avatar, Badge, CREATE_WORK_ITEM, CREATE_WORK_ITEM_LINK, CreateTask, CreateTaskCore, DEFAULT_BOARD_COLUMNS, DEFAULT_TASK_LABELS, DELETE_WORK_ITEM, DELETE_WORK_ITEM_LINK, Drawer, GET_PROJECTS, GET_WORK_ITEM, LIST_MILESTONES, LIST_WORK_ITEMS, LIST_WORK_ITEM_LINKS, LinkChip, MILESTONE_FIELDS, MOVE_WORK_ITEM, Modal, PRIORITY_META, PROJECT_SUMMARY_FIELDS, PlanMagnetProvider, PriorityIndicator, STATUS_META, StatusIcon, StatusPill, TASK_PRIORITIES, TASK_STATUSES, TASK_TYPES, TaskBoard, TaskBoardCore, TaskCard, TaskCommentsCore, TaskDetail, TaskDetailCore, TaskLabelsProvider, TaskList, TaskListCore, TaskTimelineCore, TaskToolbar, TasksPage, TasksPageCore, TypeBadge, UPDATE_WORK_ITEM, WORK_ITEM_FIELDS, WORK_ITEM_LINK_FIELDS, WORK_ITEM_SUMMARY_FIELDS, badgeVariants, cn, config, createPlanMagnetClient, enumLabel, getGraphQLEndpoint, getPlanMagnetClient, getPlanMagnetClientSafe, getPlanMagnetConfig, isPlanMagnetClientReady, mergeTaskLabels, resetPlanMagnetClient, resetPlanMagnetConfig, setPlanMagnetClient, setPlanMagnetConfig, useMilestones, usePlanMagnetContext, usePlanMagnetContextSafe, useTaskLabels, useWorkItemLinks, validateTrustedOrigin, waitForPlanMagnetClient };
4825
+ export { Avatar, BULK_UPDATE_WORK_ITEMS, Badge, CREATE_WORK_ITEM, CREATE_WORK_ITEM_LINK, CreateTask, CreateTaskCore, DEFAULT_BOARD_COLUMNS, DEFAULT_TASK_LABELS, DELETE_WORK_ITEM, DELETE_WORK_ITEM_LINK, Drawer, GET_PROJECTS, GET_WORK_ITEM, LIST_MILESTONES, LIST_WORK_ITEMS, LIST_WORK_ITEM_LINKS, LinkChip, MILESTONE_FIELDS, MOVE_WORK_ITEM, Modal, PRIORITY_META, PROJECT_SUMMARY_FIELDS, PlanMagnetProvider, PriorityIndicator, STATUS_META, SavedViewsMenu, StatusIcon, StatusPill, TASK_PRIORITIES, TASK_STATUSES, TASK_TYPES, TaskBoard, TaskBoardCore, TaskCard, TaskCommentsCore, TaskDetail, TaskDetailCore, TaskLabelsProvider, TaskList, TaskListCore, TaskTimelineCore, TaskToolbar, TasksPage, TasksPageCore, TypeBadge, UPDATE_WORK_ITEM, WORK_ITEM_FIELDS, WORK_ITEM_LINK_FIELDS, WORK_ITEM_SUMMARY_FIELDS, badgeVariants, cn, config, createPlanMagnetClient, enumLabel, getGraphQLEndpoint, getPlanMagnetClient, getPlanMagnetClientSafe, getPlanMagnetConfig, isPlanMagnetClientReady, makeStorageKey, mergeTaskLabels, resetPlanMagnetClient, resetPlanMagnetConfig, setPlanMagnetClient, setPlanMagnetConfig, useMilestones, usePlanMagnetContext, usePlanMagnetContextSafe, useSavedViews, useTaskLabels, useWorkItemLinks, validateTrustedOrigin, waitForPlanMagnetClient };
3747
4826
  //# sourceMappingURL=index.js.map
3748
4827
  //# sourceMappingURL=index.js.map