gantt-lib 0.82.0 → 0.83.0

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.mjs CHANGED
@@ -7023,6 +7023,7 @@ var TaskList = ({
7023
7023
  [resolvedColumns]
7024
7024
  );
7025
7025
  const effectiveTaskListWidth = Math.max(taskListWidth, MIN_TASK_LIST_WIDTH, resolvedColumnWidthTotal);
7026
+ const tableHeaderHeight = headerHeight + 1;
7026
7027
  return /* @__PURE__ */ jsx14(
7027
7028
  "div",
7028
7029
  {
@@ -7030,7 +7031,7 @@ var TaskList = ({
7030
7031
  className: `gantt-tl-overlay${show ? "" : " gantt-tl-hidden"}${hasRightShadow ? " gantt-tl-overlay-shadowed" : ""}`,
7031
7032
  style: { "--tasklist-width": `${effectiveTaskListWidth}px` },
7032
7033
  children: /* @__PURE__ */ jsxs11("div", { className: "gantt-tl-table", children: [
7033
- /* @__PURE__ */ jsx14("div", { className: "gantt-tl-header", style: { height: `${headerHeight + 0.5}px` }, children: resolvedColumns.map((col) => {
7034
+ /* @__PURE__ */ jsx14("div", { className: "gantt-tl-header", style: { height: `${tableHeaderHeight}px` }, children: resolvedColumns.map((col) => {
7034
7035
  if (col.id === "dependencies") {
7035
7036
  return /* @__PURE__ */ jsxs11(
7036
7037
  "div",
@@ -7214,7 +7215,7 @@ var TaskList = ({
7214
7215
  };
7215
7216
 
7216
7217
  // src/components/ResourceTimelineChart/ResourceTimelineChart.tsx
7217
- import { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo9, useRef as useRef8, useState as useState8 } from "react";
7218
+ import React12, { useCallback as useCallback7, useEffect as useEffect8, useMemo as useMemo9, useRef as useRef8, useState as useState8 } from "react";
7218
7219
 
7219
7220
  // src/utils/resourceTimelineLayout.ts
7220
7221
  var isInvalidDate = (date) => Number.isNaN(date.getTime());
@@ -7634,6 +7635,7 @@ var DEFAULT_HEADER_HEIGHT = 40;
7634
7635
  var DEFAULT_LANE_HEIGHT = 40;
7635
7636
  var DEFAULT_ROW_HEADER_WIDTH = 420;
7636
7637
  var DEFAULT_RESOURCE_ROW_GAP = 8;
7638
+ var DEFAULT_RESOURCE_GROUP_HEIGHT = 28;
7637
7639
  var ITEM_OUTER_VERTICAL_INSET = 2;
7638
7640
  var ITEM_INNER_VERTICAL_INSET = 1;
7639
7641
  var ITEM_START_HORIZONTAL_INSET = 2;
@@ -7732,6 +7734,30 @@ var RESOURCE_TYPE_CLASS_NAMES = {
7732
7734
  \u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B\u044B: "Materials",
7733
7735
  \u0414\u0440\u0443\u0433\u043E\u0435: "Other"
7734
7736
  };
7737
+ var RESOURCE_TYPE_ORDER = {
7738
+ \u041B\u044E\u0434\u0438: 0,
7739
+ \u041E\u0431\u043E\u0440\u0443\u0434\u043E\u0432\u0430\u043D\u0438\u0435: 1,
7740
+ \u041C\u0430\u0442\u0435\u0440\u0438\u0430\u043B\u044B: 2,
7741
+ \u0414\u0440\u0443\u0433\u043E\u0435: 3
7742
+ };
7743
+ var RESOURCE_SCOPE_ORDER = {
7744
+ Shared: 0,
7745
+ Project: 1
7746
+ };
7747
+ var getResourceType = (resource) => resource.type ?? "\u0414\u0440\u0443\u0433\u043E\u0435";
7748
+ var getResourceScopeOrder = (resource) => RESOURCE_SCOPE_ORDER[resource.scope ?? "Project"] ?? 99;
7749
+ var orderResourcesByType = (resources) => {
7750
+ return resources.map((resource, index) => ({ resource, index })).sort((left, right) => {
7751
+ const leftType = getResourceType(left.resource);
7752
+ const rightType = getResourceType(right.resource);
7753
+ const typeDiff = (RESOURCE_TYPE_ORDER[leftType] ?? 99) - (RESOURCE_TYPE_ORDER[rightType] ?? 99);
7754
+ if (typeDiff !== 0) {
7755
+ return typeDiff;
7756
+ }
7757
+ const scopeDiff = getResourceScopeOrder(left.resource) - getResourceScopeOrder(right.resource);
7758
+ return scopeDiff !== 0 ? scopeDiff : left.index - right.index;
7759
+ }).map(({ resource }) => resource);
7760
+ };
7735
7761
  var ResourceTypeIcon = ({ type }) => {
7736
7762
  if (type === "\u041B\u044E\u0434\u0438") {
7737
7763
  return /* @__PURE__ */ jsxs12("svg", { className: "gantt-resourceTimeline-resourceTypeIcon gantt-resourceTimeline-resourceTypeIconPeople", width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
@@ -7772,12 +7798,16 @@ var ResourceHeader = ({
7772
7798
  paddingBottom,
7773
7799
  menuCommands,
7774
7800
  onResourceChange,
7801
+ onResourceNameClick,
7775
7802
  onConflictBadgeClick
7776
7803
  }) => {
7777
7804
  const [menuOpen, setMenuOpen] = useState8(false);
7778
7805
  const [typeMenuOpen, setTypeMenuOpen] = useState8(false);
7779
7806
  const [scopeMenuOpen, setScopeMenuOpen] = useState8(false);
7780
- const [draftName, setDraftName] = useState8(resource.name);
7807
+ const [editingName, setEditingName] = useState8(false);
7808
+ const [nameValue, setNameValue] = useState8(resource.name);
7809
+ const nameInputRef = useRef8(null);
7810
+ const nameConfirmedRef = useRef8(false);
7781
7811
  const visibleCommands = useMemo9(
7782
7812
  () => menuCommands.filter((command) => command.isVisible?.(resource) ?? true),
7783
7813
  [menuCommands, resource]
@@ -7786,31 +7816,20 @@ var ResourceHeader = ({
7786
7816
  const type = resource.type ?? "\u0414\u0440\u0443\u0433\u043E\u0435";
7787
7817
  const scope = resource.scope ?? "Project";
7788
7818
  const scopeLabel = RESOURCE_SCOPE_LABELS[scope] ?? scope;
7789
- useEffect8(() => {
7790
- setDraftName(resource.name);
7791
- }, [resource.name]);
7792
7819
  const applyResourcePatch = useCallback7((patch) => {
7793
7820
  onResourceChange?.({ ...resource, ...patch });
7794
7821
  }, [onResourceChange, resource]);
7795
- const handleNameCommit = useCallback7(() => {
7796
- const nextName = draftName.trim();
7797
- if (!nextName) {
7798
- setDraftName(resource.name);
7799
- return;
7800
- }
7801
- if (nextName !== resource.name) {
7802
- applyResourcePatch({ name: nextName });
7822
+ useEffect8(() => {
7823
+ if (!editingName) {
7824
+ setNameValue(resource.name);
7803
7825
  }
7804
- }, [applyResourcePatch, draftName, resource.name]);
7805
- const handleNameKeyDown = useCallback7((event) => {
7806
- if (event.key === "Enter") {
7807
- event.preventDefault();
7808
- event.currentTarget.blur();
7809
- } else if (event.key === "Escape") {
7810
- setDraftName(resource.name);
7811
- event.currentTarget.blur();
7826
+ }, [editingName, resource.name]);
7827
+ useEffect8(() => {
7828
+ if (editingName && nameInputRef.current) {
7829
+ nameInputRef.current.focus();
7830
+ nameInputRef.current.select();
7812
7831
  }
7813
- }, [resource.name]);
7832
+ }, [editingName]);
7814
7833
  const handleCommandClick = (command, event) => {
7815
7834
  event.stopPropagation();
7816
7835
  if (command.closeOnSelect !== false) {
@@ -7818,6 +7837,42 @@ var ResourceHeader = ({
7818
7837
  }
7819
7838
  command.onSelect(resource);
7820
7839
  };
7840
+ const handleNameDoubleClick = useCallback7((event) => {
7841
+ if (!onResourceChange) {
7842
+ return;
7843
+ }
7844
+ event.stopPropagation();
7845
+ nameConfirmedRef.current = false;
7846
+ setNameValue(resource.name);
7847
+ setEditingName(true);
7848
+ }, [onResourceChange, resource.name]);
7849
+ const handleNameSave = useCallback7(() => {
7850
+ if (nameConfirmedRef.current) {
7851
+ nameConfirmedRef.current = false;
7852
+ return;
7853
+ }
7854
+ const nextName = nameValue.trim();
7855
+ if (nextName && nextName !== resource.name) {
7856
+ applyResourcePatch({ name: nextName });
7857
+ }
7858
+ setEditingName(false);
7859
+ }, [applyResourcePatch, nameValue, resource.name]);
7860
+ const handleNameCancel = useCallback7(() => {
7861
+ setNameValue(resource.name);
7862
+ setEditingName(false);
7863
+ }, [resource.name]);
7864
+ const handleNameKeyDown = useCallback7((event) => {
7865
+ if (event.key === "Enter") {
7866
+ nameConfirmedRef.current = true;
7867
+ const nextName = nameValue.trim();
7868
+ if (nextName && nextName !== resource.name) {
7869
+ applyResourcePatch({ name: nextName });
7870
+ }
7871
+ setEditingName(false);
7872
+ } else if (event.key === "Escape") {
7873
+ handleNameCancel();
7874
+ }
7875
+ }, [applyResourcePatch, handleNameCancel, nameValue, resource.name]);
7821
7876
  return /* @__PURE__ */ jsxs12(
7822
7877
  "div",
7823
7878
  {
@@ -7864,18 +7919,31 @@ var ResourceHeader = ({
7864
7919
  option
7865
7920
  )) })
7866
7921
  ] }),
7867
- /* @__PURE__ */ jsx15(
7868
- "textarea",
7922
+ editingName ? /* @__PURE__ */ jsx15(
7923
+ Input,
7869
7924
  {
7870
- className: "gantt-resourceTimeline-resourceNameInput",
7871
- value: draftName,
7872
- disabled: !onResourceChange,
7873
- "aria-label": `\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}`,
7874
- rows: 2,
7875
- onChange: (event) => setDraftName(event.target.value),
7876
- onBlur: handleNameCommit,
7925
+ ref: nameInputRef,
7926
+ value: nameValue,
7927
+ onChange: (event) => setNameValue(event.target.value),
7928
+ onBlur: handleNameSave,
7877
7929
  onKeyDown: handleNameKeyDown,
7878
- onClick: (event) => event.stopPropagation()
7930
+ onClick: (event) => event.stopPropagation(),
7931
+ "aria-label": `\u041D\u043E\u0432\u043E\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}`,
7932
+ className: "gantt-tl-name-input gantt-resourceTimeline-resourceNameInput"
7933
+ }
7934
+ ) : /* @__PURE__ */ jsx15(
7935
+ "button",
7936
+ {
7937
+ type: "button",
7938
+ className: "gantt-resourceTimeline-resourceNameButton",
7939
+ "aria-label": `\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}`,
7940
+ title: resource.name,
7941
+ onClick: (event) => {
7942
+ event.stopPropagation();
7943
+ onResourceNameClick?.(resourceId);
7944
+ },
7945
+ onDoubleClick: handleNameDoubleClick,
7946
+ children: resource.name
7879
7947
  }
7880
7948
  )
7881
7949
  ] }),
@@ -7919,15 +7987,15 @@ var ResourceHeader = ({
7919
7987
  "aria-label": `\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0440\u0435\u0441\u0443\u0440\u0441\u0430 ${resource.name}: ${assignmentCount}, ${workedDays} \u0434\u043D.`,
7920
7988
  children: [
7921
7989
  /* @__PURE__ */ jsxs12("span", { className: "gantt-resourceTimeline-resourceWorkedDays", children: [
7922
- workedDays,
7923
- " \u0434\u043D."
7990
+ /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceMetricValue", children: workedDays > 0 ? workedDays : "-" }),
7991
+ /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceMetricLabel", children: workedDays > 0 ? "\u0434\u043D." : "" })
7924
7992
  ] }),
7925
7993
  /* @__PURE__ */ jsxs12("span", { className: "gantt-resourceTimeline-resourceAssignmentCount", children: [
7926
- /* @__PURE__ */ jsxs12("svg", { className: "gantt-resourceTimeline-resourceAssignmentIcon", width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
7994
+ /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceMetricValue", children: assignmentCount > 0 ? assignmentCount : "" }),
7995
+ assignmentCount > 0 && /* @__PURE__ */ jsxs12("svg", { className: "gantt-resourceTimeline-resourceAssignmentIcon", width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", children: [
7927
7996
  /* @__PURE__ */ jsx15("rect", { width: "15", height: "5", x: "4", y: "5", rx: "2" }),
7928
7997
  /* @__PURE__ */ jsx15("rect", { width: "10", height: "5", x: "4", y: "14", rx: "2" })
7929
- ] }),
7930
- /* @__PURE__ */ jsx15("span", { children: assignmentCount })
7998
+ ] })
7931
7999
  ] })
7932
8000
  ]
7933
8001
  }
@@ -8052,6 +8120,7 @@ function ResourceTimelineChart({
8052
8120
  customDays,
8053
8121
  isWeekend: isWeekend3,
8054
8122
  businessDays = true,
8123
+ resourceGrouping = false,
8055
8124
  readonly,
8056
8125
  disableResourceReassignment,
8057
8126
  renderItem,
@@ -8069,9 +8138,12 @@ function ResourceTimelineChart({
8069
8138
  const gridRef = useRef8(null);
8070
8139
  const panStateRef = useRef8(null);
8071
8140
  const conflictNavigationIndexRef = useRef8(/* @__PURE__ */ new Map());
8141
+ const resourceNavigationIndexRef = useRef8(/* @__PURE__ */ new Map());
8072
8142
  const conflictHighlightTimeoutRef = useRef8(null);
8073
8143
  const [isCreatingResource, setIsCreatingResource] = useState8(false);
8144
+ const [creatingResourceGroupType, setCreatingResourceGroupType] = useState8(null);
8074
8145
  const [activeConflictItemId, setActiveConflictItemId] = useState8(null);
8146
+ const [resourceColumnHasRightShadow, setResourceColumnHasRightShadow] = useState8(false);
8075
8147
  const validItems = useMemo9(() => collectValidItems(resources), [resources]);
8076
8148
  const dateRange = useMemo9(() => getMultiMonthDays(validItems), [validItems]);
8077
8149
  const monthStart = useMemo9(() => {
@@ -8084,6 +8156,10 @@ function ResourceTimelineChart({
8084
8156
  );
8085
8157
  const gridWidth = useMemo9(() => Math.round(dateRange.length * dayWidth), [dateRange.length, dayWidth]);
8086
8158
  const effectiveRowHeaderWidth = Math.max(rowHeaderWidth, DEFAULT_ROW_HEADER_WIDTH);
8159
+ const orderedResources = useMemo9(
8160
+ () => resourceGrouping === "type" ? orderResourcesByType(resources) : resources,
8161
+ [resourceGrouping, resources]
8162
+ );
8087
8163
  const workedDaysByResourceId = useMemo9(() => {
8088
8164
  const map = /* @__PURE__ */ new Map();
8089
8165
  for (const resource of resources) {
@@ -8104,14 +8180,87 @@ function ResourceTimelineChart({
8104
8180
  return map;
8105
8181
  }, [businessDays, resources, weekendPredicate]);
8106
8182
  const layout = useMemo9(
8107
- () => layoutResourceTimelineItems(resources, {
8183
+ () => layoutResourceTimelineItems(orderedResources, {
8108
8184
  monthStart,
8109
8185
  dayWidth,
8110
8186
  laneHeight,
8111
8187
  rowGap: DEFAULT_RESOURCE_ROW_GAP
8112
8188
  }),
8113
- [resources, monthStart, dayWidth, laneHeight]
8189
+ [orderedResources, monthStart, dayWidth, laneHeight]
8114
8190
  );
8191
+ const canAddResource = enableAddResource && Boolean(onAddResource);
8192
+ const resourceAddRowHeight = laneHeight + DEFAULT_RESOURCE_ROW_GAP;
8193
+ const displayLayout = useMemo9(() => {
8194
+ if (resourceGrouping !== "type") {
8195
+ return {
8196
+ rows: layout.rows,
8197
+ items: layout.items,
8198
+ groupHeaders: [],
8199
+ groupAddRows: [],
8200
+ totalHeight: layout.totalHeight
8201
+ };
8202
+ }
8203
+ const countsByType = /* @__PURE__ */ new Map();
8204
+ for (const row of layout.rows) {
8205
+ const type = getResourceType(row.resource);
8206
+ countsByType.set(type, (countsByType.get(type) ?? 0) + 1);
8207
+ }
8208
+ let offset = 0;
8209
+ let previousType = null;
8210
+ const rowTopOffsets = /* @__PURE__ */ new Map();
8211
+ const groupHeaders = [];
8212
+ const groupAddRows = [];
8213
+ const rows = layout.rows.map((row) => {
8214
+ const type = getResourceType(row.resource);
8215
+ if (type !== previousType) {
8216
+ if (previousType !== null && creatingResourceGroupType === previousType) {
8217
+ groupAddRows.push({
8218
+ key: previousType,
8219
+ top: row.resourceRowTop + offset,
8220
+ height: resourceAddRowHeight
8221
+ });
8222
+ offset += resourceAddRowHeight;
8223
+ }
8224
+ groupHeaders.push({
8225
+ key: type,
8226
+ label: type,
8227
+ count: countsByType.get(type) ?? 0,
8228
+ top: row.resourceRowTop + offset,
8229
+ height: DEFAULT_RESOURCE_GROUP_HEIGHT
8230
+ });
8231
+ offset += DEFAULT_RESOURCE_GROUP_HEIGHT;
8232
+ previousType = type;
8233
+ }
8234
+ rowTopOffsets.set(row.resourceId, offset);
8235
+ return {
8236
+ ...row,
8237
+ resourceRowTop: row.resourceRowTop + offset
8238
+ };
8239
+ });
8240
+ if (previousType !== null && creatingResourceGroupType === previousType) {
8241
+ groupAddRows.push({
8242
+ key: previousType,
8243
+ top: layout.totalHeight + offset,
8244
+ height: resourceAddRowHeight
8245
+ });
8246
+ offset += resourceAddRowHeight;
8247
+ }
8248
+ const items = layout.items.map((item) => {
8249
+ const itemOffset = rowTopOffsets.get(item.resourceId) ?? 0;
8250
+ return {
8251
+ ...item,
8252
+ resourceRowTop: item.resourceRowTop + itemOffset,
8253
+ top: item.top + itemOffset
8254
+ };
8255
+ });
8256
+ return {
8257
+ rows,
8258
+ items,
8259
+ groupHeaders,
8260
+ groupAddRows,
8261
+ totalHeight: layout.totalHeight + offset
8262
+ };
8263
+ }, [creatingResourceGroupType, layout, resourceAddRowHeight, resourceGrouping]);
8115
8264
  const todayInRange = useMemo9(() => {
8116
8265
  const now = /* @__PURE__ */ new Date();
8117
8266
  const today = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
@@ -8119,16 +8268,16 @@ function ResourceTimelineChart({
8119
8268
  }, [dateRange]);
8120
8269
  const itemsByResourceId = useMemo9(() => {
8121
8270
  const map = /* @__PURE__ */ new Map();
8122
- for (const item of layout.items) {
8271
+ for (const item of displayLayout.items) {
8123
8272
  const next = map.get(item.resourceId) ?? [];
8124
8273
  next.push(item);
8125
8274
  map.set(item.resourceId, next);
8126
8275
  }
8127
8276
  return map;
8128
- }, [layout.items]);
8277
+ }, [displayLayout.items]);
8129
8278
  const conflictItemsByResourceId = useMemo9(() => {
8130
8279
  const map = /* @__PURE__ */ new Map();
8131
- for (const item of layout.items) {
8280
+ for (const item of displayLayout.items) {
8132
8281
  if (item.conflictRanges.length === 0) {
8133
8282
  continue;
8134
8283
  }
@@ -8142,21 +8291,28 @@ function ResourceTimelineChart({
8142
8291
  );
8143
8292
  }
8144
8293
  return map;
8145
- }, [layout.items]);
8146
- const canAddResource = enableAddResource && Boolean(onAddResource);
8147
- const resourceAddRowHeight = laneHeight + DEFAULT_RESOURCE_ROW_GAP;
8148
- const displayTotalHeight = layout.totalHeight + (canAddResource ? resourceAddRowHeight : 0);
8294
+ }, [displayLayout.items]);
8295
+ const displayTotalHeight = displayLayout.totalHeight + (canAddResource ? resourceAddRowHeight : 0);
8296
+ const timelineHeaderHeight = headerHeight + 1;
8149
8297
  const handleConfirmNewResource = useCallback7((name) => {
8150
8298
  onAddResource?.({
8151
8299
  id: crypto.randomUUID(),
8152
8300
  name,
8153
- type: "\u0414\u0440\u0443\u0433\u043E\u0435",
8301
+ type: creatingResourceGroupType ?? "\u0414\u0440\u0443\u0433\u043E\u0435",
8154
8302
  scope: "Project",
8155
8303
  items: []
8156
8304
  });
8157
8305
  setIsCreatingResource(false);
8158
- }, [onAddResource]);
8159
- const handleCancelNewResource = useCallback7(() => setIsCreatingResource(false), []);
8306
+ setCreatingResourceGroupType(null);
8307
+ }, [creatingResourceGroupType, onAddResource]);
8308
+ const handleStartAddResourceToGroup = useCallback7((type) => {
8309
+ setIsCreatingResource(false);
8310
+ setCreatingResourceGroupType(type);
8311
+ }, []);
8312
+ const handleCancelNewResource = useCallback7(() => {
8313
+ setIsCreatingResource(false);
8314
+ setCreatingResourceGroupType(null);
8315
+ }, []);
8160
8316
  const handleConflictBadgeClick = useCallback7((resourceId) => {
8161
8317
  const conflictItems = conflictItemsByResourceId.get(resourceId) ?? [];
8162
8318
  if (conflictItems.length === 0) {
@@ -8182,10 +8338,31 @@ function ResourceTimelineChart({
8182
8338
  conflictHighlightTimeoutRef.current = null;
8183
8339
  }, 1600);
8184
8340
  }, [conflictItemsByResourceId, dayWidth, laneHeight]);
8341
+ const handleResourceNameClick = useCallback7((resourceId) => {
8342
+ const resourceItems = itemsByResourceId.get(resourceId) ?? [];
8343
+ if (resourceItems.length === 0) {
8344
+ return;
8345
+ }
8346
+ const orderedItems = [...resourceItems].sort(
8347
+ (left, right) => left.left - right.left || left.top - right.top || left.itemId.localeCompare(right.itemId)
8348
+ );
8349
+ const currentIndex = resourceNavigationIndexRef.current.get(resourceId) ?? 0;
8350
+ const targetItem = orderedItems[currentIndex % orderedItems.length];
8351
+ resourceNavigationIndexRef.current.set(resourceId, (currentIndex + 1) % orderedItems.length);
8352
+ const container = scrollContainerRef.current;
8353
+ if (!container || !targetItem) {
8354
+ return;
8355
+ }
8356
+ container.scrollTo({
8357
+ left: Math.max(0, Math.round(targetItem.left - dayWidth * 2)),
8358
+ top: Math.max(0, Math.round(targetItem.top - laneHeight)),
8359
+ behavior: "smooth"
8360
+ });
8361
+ }, [dayWidth, itemsByResourceId, laneHeight]);
8185
8362
  const { preview, startDrag } = useResourceItemDrag({
8186
8363
  dayWidth,
8187
8364
  monthStart,
8188
- rows: layout.rows,
8365
+ rows: displayLayout.rows,
8189
8366
  gridElementRef: gridRef,
8190
8367
  readonly,
8191
8368
  disableResourceReassignment,
@@ -8193,6 +8370,18 @@ function ResourceTimelineChart({
8193
8370
  weekendPredicate,
8194
8371
  onResourceItemMove
8195
8372
  });
8373
+ useEffect8(() => {
8374
+ const container = scrollContainerRef.current;
8375
+ if (!container) return;
8376
+ const updateShadow = () => {
8377
+ setResourceColumnHasRightShadow(container.scrollLeft > 0);
8378
+ };
8379
+ updateShadow();
8380
+ container.addEventListener("scroll", updateShadow, { passive: true });
8381
+ return () => {
8382
+ container.removeEventListener("scroll", updateShadow);
8383
+ };
8384
+ }, []);
8196
8385
  const handlePanStart = useCallback7((event) => {
8197
8386
  if (event.button !== 0) {
8198
8387
  return;
@@ -8250,6 +8439,16 @@ function ResourceTimelineChart({
8250
8439
  window.removeEventListener("mouseup", handlePanEnd);
8251
8440
  };
8252
8441
  }, [allowVerticalPan]);
8442
+ useEffect8(() => {
8443
+ const nextNavigation = /* @__PURE__ */ new Map();
8444
+ for (const [resourceId, resourceItems] of itemsByResourceId.entries()) {
8445
+ if (resourceItems.length === 0) {
8446
+ continue;
8447
+ }
8448
+ nextNavigation.set(resourceId, (resourceNavigationIndexRef.current.get(resourceId) ?? 0) % resourceItems.length);
8449
+ }
8450
+ resourceNavigationIndexRef.current = nextNavigation;
8451
+ }, [itemsByResourceId]);
8253
8452
  useEffect8(() => {
8254
8453
  return () => {
8255
8454
  if (conflictHighlightTimeoutRef.current) {
@@ -8273,14 +8472,14 @@ function ResourceTimelineChart({
8273
8472
  /* @__PURE__ */ jsxs12(
8274
8473
  "div",
8275
8474
  {
8276
- className: "gantt-resourceTimeline-resourceColumn",
8475
+ className: `gantt-resourceTimeline-resourceColumn${resourceColumnHasRightShadow ? " gantt-resourceTimeline-resourceColumnShadowed" : ""}`,
8277
8476
  style: { width: `${effectiveRowHeaderWidth}px`, minWidth: `${effectiveRowHeaderWidth}px` },
8278
8477
  children: [
8279
8478
  /* @__PURE__ */ jsxs12(
8280
8479
  "div",
8281
8480
  {
8282
8481
  className: "gantt-resourceTimeline-corner",
8283
- style: { height: `${headerHeight + 0.5}px` },
8482
+ style: { height: `${timelineHeaderHeight}px` },
8284
8483
  children: [
8285
8484
  /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceHeaderCell gantt-resourceTimeline-resourceHeaderNumber", children: "#" }),
8286
8485
  /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceHeaderCell gantt-resourceTimeline-resourceHeaderName", children: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435" }),
@@ -8290,7 +8489,62 @@ function ResourceTimelineChart({
8290
8489
  ]
8291
8490
  }
8292
8491
  ),
8293
- layout.rows.map((row, rowIndex) => /* @__PURE__ */ jsx15(
8492
+ displayLayout.groupHeaders.length > 0 ? displayLayout.groupHeaders.map((group) => /* @__PURE__ */ jsxs12(React12.Fragment, { children: [
8493
+ /* @__PURE__ */ jsxs12(
8494
+ "div",
8495
+ {
8496
+ className: "gantt-resourceTimeline-resourceGroupHeader",
8497
+ style: { height: `${group.height}px` },
8498
+ children: [
8499
+ /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceGroupTitle", children: group.label }),
8500
+ /* @__PURE__ */ jsx15("span", { className: "gantt-resourceTimeline-resourceGroupCount", children: group.count }),
8501
+ canAddResource && /* @__PURE__ */ jsx15(
8502
+ "button",
8503
+ {
8504
+ type: "button",
8505
+ className: "gantt-resourceTimeline-resourceGroupAddButton",
8506
+ "aria-label": `\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441 \u0432 \u0433\u0440\u0443\u043F\u043F\u0443 ${group.label}`,
8507
+ title: `\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441 \u0432 \u0433\u0440\u0443\u043F\u043F\u0443 ${group.label}`,
8508
+ onClick: (event) => {
8509
+ event.stopPropagation();
8510
+ handleStartAddResourceToGroup(group.key);
8511
+ },
8512
+ children: "+"
8513
+ }
8514
+ )
8515
+ ]
8516
+ }
8517
+ ),
8518
+ displayLayout.rows.filter((row) => getResourceType(row.resource) === group.key).map((row) => {
8519
+ const rowIndex = displayLayout.rows.findIndex((candidate) => candidate.resourceId === row.resourceId);
8520
+ return /* @__PURE__ */ jsx15(
8521
+ ResourceHeader,
8522
+ {
8523
+ resource: row.resource,
8524
+ resourceId: row.resourceId,
8525
+ rowIndex,
8526
+ conflictCount: row.conflictCount,
8527
+ workedDays: workedDaysByResourceId.get(row.resourceId) ?? 0,
8528
+ assignmentCount: row.resource.items.length,
8529
+ height: row.resourceRowHeight + DEFAULT_RESOURCE_ROW_GAP,
8530
+ paddingBottom: DEFAULT_RESOURCE_ROW_GAP,
8531
+ menuCommands: resourceMenuCommands,
8532
+ onResourceChange,
8533
+ onResourceNameClick: handleResourceNameClick,
8534
+ onConflictBadgeClick: handleConflictBadgeClick
8535
+ },
8536
+ row.resourceId
8537
+ );
8538
+ }),
8539
+ displayLayout.groupAddRows.some((row) => row.key === group.key) && /* @__PURE__ */ jsx15(
8540
+ NewResourceRow,
8541
+ {
8542
+ height: resourceAddRowHeight,
8543
+ onConfirm: handleConfirmNewResource,
8544
+ onCancel: handleCancelNewResource
8545
+ }
8546
+ )
8547
+ ] }, group.key)) : displayLayout.rows.map((row, rowIndex) => /* @__PURE__ */ jsx15(
8294
8548
  ResourceHeader,
8295
8549
  {
8296
8550
  resource: row.resource,
@@ -8303,6 +8557,7 @@ function ResourceTimelineChart({
8303
8557
  paddingBottom: DEFAULT_RESOURCE_ROW_GAP,
8304
8558
  menuCommands: resourceMenuCommands,
8305
8559
  onResourceChange,
8560
+ onResourceNameClick: handleResourceNameClick,
8306
8561
  onConflictBadgeClick: handleConflictBadgeClick
8307
8562
  },
8308
8563
  row.resourceId
@@ -8320,7 +8575,10 @@ function ResourceTimelineChart({
8320
8575
  className: "gantt-resourceTimeline-addResourceButton",
8321
8576
  type: "button",
8322
8577
  style: { height: `${resourceAddRowHeight}px` },
8323
- onClick: () => setIsCreatingResource(true),
8578
+ onClick: () => {
8579
+ setCreatingResourceGroupType(null);
8580
+ setIsCreatingResource(true);
8581
+ },
8324
8582
  children: "+ \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0440\u0435\u0441\u0443\u0440\u0441"
8325
8583
  }
8326
8584
  ))
@@ -8333,16 +8591,23 @@ function ResourceTimelineChart({
8333
8591
  className: "gantt-resourceTimeline-chartSurface",
8334
8592
  style: { minWidth: `${gridWidth}px` },
8335
8593
  children: [
8336
- /* @__PURE__ */ jsx15("div", { className: "gantt-resourceTimeline-stickyHeader", style: { width: `${gridWidth}px` }, children: /* @__PURE__ */ jsx15(
8337
- TimeScaleHeader_default,
8594
+ /* @__PURE__ */ jsx15(
8595
+ "div",
8338
8596
  {
8339
- days: dateRange,
8340
- dayWidth,
8341
- headerHeight,
8342
- viewMode,
8343
- isCustomWeekend: weekendPredicate
8597
+ className: "gantt-resourceTimeline-stickyHeader",
8598
+ style: { width: `${gridWidth}px`, height: `${timelineHeaderHeight}px` },
8599
+ children: /* @__PURE__ */ jsx15(
8600
+ TimeScaleHeader_default,
8601
+ {
8602
+ days: dateRange,
8603
+ dayWidth,
8604
+ headerHeight,
8605
+ viewMode,
8606
+ isCustomWeekend: weekendPredicate
8607
+ }
8608
+ )
8344
8609
  }
8345
- ) }),
8610
+ ),
8346
8611
  /* @__PURE__ */ jsxs12(
8347
8612
  "div",
8348
8613
  {
@@ -8361,7 +8626,19 @@ function ResourceTimelineChart({
8361
8626
  }
8362
8627
  ),
8363
8628
  todayInRange && /* @__PURE__ */ jsx15(TodayIndicator_default, { monthStart, dayWidth }),
8364
- layout.rows.map((row) => /* @__PURE__ */ jsx15(
8629
+ displayLayout.groupHeaders.map((group) => /* @__PURE__ */ jsx15(
8630
+ "div",
8631
+ {
8632
+ className: "gantt-resourceTimeline-row gantt-resourceTimeline-groupRow",
8633
+ "data-resource-group": group.key,
8634
+ style: {
8635
+ top: `${group.top}px`,
8636
+ height: `${group.height}px`
8637
+ }
8638
+ },
8639
+ `group-row-${group.key}`
8640
+ )),
8641
+ displayLayout.rows.map((row) => /* @__PURE__ */ jsx15(
8365
8642
  "div",
8366
8643
  {
8367
8644
  className: "gantt-resourceTimeline-row",
@@ -8379,11 +8656,23 @@ function ResourceTimelineChart({
8379
8656
  className: "gantt-resourceTimeline-row gantt-resourceTimeline-rowNew",
8380
8657
  "data-resource-add-row": "true",
8381
8658
  style: {
8382
- top: `${layout.totalHeight}px`,
8659
+ top: `${displayLayout.totalHeight}px`,
8383
8660
  height: `${resourceAddRowHeight}px`
8384
8661
  }
8385
8662
  }
8386
8663
  ),
8664
+ displayLayout.groupAddRows.map((row) => /* @__PURE__ */ jsx15(
8665
+ "div",
8666
+ {
8667
+ className: "gantt-resourceTimeline-row gantt-resourceTimeline-rowNew",
8668
+ "data-resource-group-add-row": row.key,
8669
+ style: {
8670
+ top: `${row.top}px`,
8671
+ height: `${row.height}px`
8672
+ }
8673
+ },
8674
+ `group-add-row-${row.key}`
8675
+ )),
8387
8676
  Array.from(itemsByResourceId.values()).flatMap(
8388
8677
  (resourceItems) => resourceItems.map((layoutItem) => {
8389
8678
  const customClassName = getItemClassName?.(layoutItem.item);
@@ -8990,6 +9279,7 @@ function TaskGanttChartInner(props, ref) {
8990
9279
  () => visibleTasks.length * rowHeight,
8991
9280
  [visibleTasks.length, rowHeight]
8992
9281
  );
9282
+ const timelineHeaderHeight = headerHeight + 1;
8993
9283
  const monthStart = useMemo10(() => {
8994
9284
  if (dateRange.length === 0) {
8995
9285
  return new Date(Date.UTC((/* @__PURE__ */ new Date()).getUTCFullYear(), (/* @__PURE__ */ new Date()).getUTCMonth(), 1));
@@ -9472,16 +9762,23 @@ function TaskGanttChartInner(props, ref) {
9472
9762
  className: showChart ? "gantt-chartSurface" : "gantt-chartSurface gantt-chart-hidden",
9473
9763
  style: { minWidth: `${gridWidth}px`, flex: 1, display: showChart ? void 0 : "none" },
9474
9764
  children: [
9475
- /* @__PURE__ */ jsx16("div", { className: "gantt-stickyHeader", style: { width: `${gridWidth}px` }, children: /* @__PURE__ */ jsx16(
9476
- TimeScaleHeader_default,
9765
+ /* @__PURE__ */ jsx16(
9766
+ "div",
9477
9767
  {
9478
- days: dateRange,
9479
- dayWidth,
9480
- headerHeight,
9481
- viewMode,
9482
- isCustomWeekend
9768
+ className: "gantt-stickyHeader",
9769
+ style: { width: `${gridWidth}px`, height: `${timelineHeaderHeight}px` },
9770
+ children: /* @__PURE__ */ jsx16(
9771
+ TimeScaleHeader_default,
9772
+ {
9773
+ days: dateRange,
9774
+ dayWidth,
9775
+ headerHeight,
9776
+ viewMode,
9777
+ isCustomWeekend
9778
+ }
9779
+ )
9483
9780
  }
9484
- ) }),
9781
+ ),
9485
9782
  /* @__PURE__ */ jsxs13(
9486
9783
  "div",
9487
9784
  {