@evergis/react 4.0.143 → 4.0.144

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
@@ -11407,10 +11407,17 @@ const toSchemaAttribute = (params) => {
11407
11407
  attributeName,
11408
11408
  // Тип принадлежит источнику: конфиг задаёт его только там, где источника нет.
11409
11409
  type: source?.type ?? description?.type ?? DEFAULT_ATTRIBUTE_TYPE,
11410
+ // Уточнение типа — целиком за конфигом: у атрибута источника поля `subType` нет.
11411
+ subType: description?.subType,
11410
11412
  alias: description?.alias ?? source?.alias ?? attributeName,
11411
11413
  description: description?.description ?? source?.description,
11412
11414
  isEditable: resolveIsEditable(params),
11413
11415
  stringFormat: resolveStringFormat(description, source),
11416
+ // Раскладка колонки живёт только в конфиге контейнера: источник о ширине таблицы не знает.
11417
+ width: description?.width,
11418
+ resizable: description?.resizable,
11419
+ multiline: description?.multiline,
11420
+ style: description?.style,
11414
11421
  };
11415
11422
  };
11416
11423
  /**
@@ -15441,6 +15448,8 @@ const TIME_FORMAT = /hh:mm/;
15441
15448
  * закрытие редактора по клику снаружи обязано этот контейнер пропускать.
15442
15449
  */
15443
15450
  const PORTAL_ROOT_SELECTOR = "#portal-root";
15451
+ /** Ниже этой ширины колонку не ужать мышью: уже неё в ячейке не остаётся места под значение. */
15452
+ const MIN_COLUMN_WIDTH = 48;
15444
15453
 
15445
15454
  /**
15446
15455
  * Собственный бокс таблицы. Без размеров в `options` представления узел ничего не меняет:
@@ -15497,10 +15506,13 @@ const TableHead = styled.thead.withConfig({ displayName: "TableHead", componentI
15497
15506
  z-index: 1;
15498
15507
  `;
15499
15508
  const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", componentId: "sc-1m6xil2" }) `
15509
+ /* Относительное позиционирование держит захват границы колонки (TableResizeHandle). */
15510
+ position: relative;
15500
15511
  padding: 0.375rem 0.5rem;
15501
15512
  text-align: left;
15502
15513
  font-weight: 600;
15503
15514
  white-space: nowrap;
15515
+ overflow: hidden;
15504
15516
  color: ${({ theme }) => theme.palette.textSecondary};
15505
15517
  /* Рамка принадлежит ячейке (рамки раздельные), поэтому едет вместе с липкой шапкой. */
15506
15518
  border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
@@ -15511,10 +15523,37 @@ const TableHeadCell = styled.th.withConfig({ displayName: "TableHeadCell", compo
15511
15523
  user-select: none;
15512
15524
  `}
15513
15525
  `;
15526
+ /**
15527
+ * Содержимое заголовка. Псевдоним обрезается многоточием, значок сортировки не сжимается:
15528
+ * у колонки с заданной шириной длинный псевдоним иначе выдавил бы значок за край ячейки.
15529
+ */
15514
15530
  const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent", componentId: "sc-8hzcop" }) `
15515
- display: inline-flex;
15531
+ display: flex;
15516
15532
  align-items: center;
15517
15533
  gap: 0.25rem;
15534
+ min-width: 0;
15535
+ overflow: hidden;
15536
+ text-overflow: ellipsis;
15537
+ `;
15538
+ /**
15539
+ * Захват правой границы колонки. Лежит поверх края ячейки и ловит указатель сам, поэтому
15540
+ * перетаскивание не доходит до заголовка и не переключает сортировку.
15541
+ *
15542
+ * Ширина в пикселах, а не в rem: это зона попадания курсора, и от размера шрифта она не зависит.
15543
+ */
15544
+ const TableResizeHandle = styled.span.withConfig({ displayName: "TableResizeHandle", componentId: "sc-1g0q105" }) `
15545
+ position: absolute;
15546
+ top: 0;
15547
+ right: 0;
15548
+ bottom: 0;
15549
+ width: 8px;
15550
+ cursor: col-resize;
15551
+ touch-action: none;
15552
+ user-select: none;
15553
+
15554
+ :hover {
15555
+ background: ${({ theme }) => theme.palette.elementDeep};
15556
+ }
15518
15557
  `;
15519
15558
  /**
15520
15559
  * Место под значок сортировки. Занято всегда, пока сортировка включена, а сам значок только
@@ -15522,7 +15561,7 @@ const TableHeadContent = styled.span.withConfig({ displayName: "TableHeadContent
15522
15561
  * клик по заголовку, вместе с телом. Оба направления рисуются значком в 1rem, поэтому
15523
15562
  * переключение asc/des ширину тоже не трогает.
15524
15563
  */
15525
- const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-z378vf" }) `
15564
+ const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", componentId: "sc-1ceykgw" }) `
15526
15565
  display: inline-flex;
15527
15566
  visibility: ${({ $active }) => ($active ? "visible" : "hidden")};
15528
15567
  `;
@@ -15531,36 +15570,54 @@ const TableSortSlot = styled.span.withConfig({ displayName: "TableSortSlot", com
15531
15570
  * её нижнюю кромку и сквозь прозрачную шапку не проступают. Величину пишет {@link useHeadOverlap},
15532
15571
  * без неё отсечка нулевая и разметка не меняется.
15533
15572
  */
15534
- const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-1pxpl5v" }) `
15573
+ const TableBody = styled.tbody.withConfig({ displayName: "TableBody", componentId: "sc-c9drzt" }) `
15535
15574
  clip-path: inset(var(${HEAD_OVERLAP_VARIABLE}, 0px) 0 0 0);
15536
15575
  `;
15537
15576
  /**
15538
15577
  * Строка. Разделитель рисуют её ячейки: собственная рамка строки при раздельных рамках не
15539
15578
  * рисуется вовсе, а при схлопнутых её рисовала бы таблица — и отсечка тела её бы не резала.
15540
15579
  */
15541
- const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-j62zvd" }) `
15580
+ const TableRow = styled.tr.withConfig({ displayName: "TableRow", componentId: "sc-155nkv8" }) `
15542
15581
  td {
15543
15582
  border-bottom: 1px solid ${({ theme }) => theme.palette.elementDeep};
15544
15583
  }
15545
15584
  `;
15546
- const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-ziu5rk" }) `
15585
+ const TableCellWrapper = styled.td.withConfig({ displayName: "TableCellWrapper", componentId: "sc-7ueth1" }) `
15547
15586
  padding: 0.125rem 0.25rem;
15548
15587
  vertical-align: middle;
15549
15588
  /* Предел снимает заданная ширина: с колонками по содержимому обрезать значение незачем. */
15550
15589
  max-width: var(--table-cell-max-width, 20rem);
15551
15590
  `;
15552
15591
  /** Колонка действий: узкая, не растягивается содержимым. */
15553
- const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-18w13t3" }) `
15592
+ const TableActionsCell = styled(TableCellWrapper).withConfig({ displayName: "TableActionsCell", componentId: "sc-1m7p53b" }) `
15554
15593
  width: 2rem;
15555
15594
  text-align: right;
15556
15595
  `;
15557
- const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "sc-ok630s" }) `
15596
+ /**
15597
+ * Как значение ведёт себя, когда не помещается в колонку: режется многоточием в одну строку
15598
+ * или переносится по словам, растягивая строку таблицы вниз (`multiline` атрибута).
15599
+ *
15600
+ * Перенос разрешаем и посреди слова: колонка бывает уже одного длинного слова (ссылка, артикул),
15601
+ * и без этого оно вылезло бы за заданную ширину, сделав её бессмысленной.
15602
+ */
15603
+ const cellWrapMixin = styled.css `
15604
+ ${({ $multiline }) => $multiline
15605
+ ? styled.css `
15606
+ white-space: normal;
15607
+ overflow-wrap: anywhere;
15608
+ `
15609
+ : styled.css `
15610
+ overflow: hidden;
15611
+ text-overflow: ellipsis;
15612
+ white-space: nowrap;
15613
+ `}
15614
+ `;
15615
+ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "sc-19uakee" }) `
15558
15616
  padding: 0.375rem 0.25rem;
15559
15617
  /* Та же высота строки, что у редактора — иначе строка подпрыгивала бы по вертикали. */
15560
15618
  line-height: 1.25rem;
15561
- overflow: hidden;
15562
- text-overflow: ellipsis;
15563
- white-space: nowrap;
15619
+
15620
+ ${cellWrapMixin};
15564
15621
  `;
15565
15622
  /**
15566
15623
  * Редактируемая ячейка вне фокуса. Показывает значение по `stringFormat`, а редактор
@@ -15569,7 +15626,7 @@ const CellText = styled.div.withConfig({ displayName: "CellText", componentId: "
15569
15626
  * Кнопка, а не просто текст, именно ради фокуса: она — таб-стоп ячейки, и Tab по таблице
15570
15627
  * открывает редактор с курсором внутри.
15571
15628
  */
15572
- const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-98k9cz" }) `
15629
+ const CellButton = styled.button.withConfig({ displayName: "CellButton", componentId: "sc-16lgve4" }) `
15573
15630
  width: 100%;
15574
15631
  padding: 0.375rem 0.25rem;
15575
15632
  line-height: 1.25rem;
@@ -15580,15 +15637,14 @@ const CellButton = styled.button.withConfig({ displayName: "CellButton", compone
15580
15637
  color: inherit;
15581
15638
  text-align: left;
15582
15639
  cursor: text;
15583
- overflow: hidden;
15584
- text-overflow: ellipsis;
15585
- white-space: nowrap;
15640
+
15641
+ ${cellWrapMixin};
15586
15642
 
15587
15643
  :hover {
15588
15644
  border-color: ${({ theme }) => theme.palette.elementDeep};
15589
15645
  }
15590
15646
  `;
15591
- const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-7tff41" }) `
15647
+ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder", componentId: "sc-1jnsjad" }) `
15592
15648
  color: ${({ theme }) => theme.palette.textSecondary};
15593
15649
  `;
15594
15650
  /**
@@ -15597,20 +15653,21 @@ const CellPlaceholder = styled.span.withConfig({ displayName: "CellPlaceholder",
15597
15653
  * Копия остаётся в потоке и держит ширину колонки ровно такой, какой она была до клика, —
15598
15654
  * иначе таблица дёргалась бы на каждый вход в правку и выход из неё.
15599
15655
  */
15600
- const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-qa22du" }) `
15656
+ const CellEditor = styled.div.withConfig({ displayName: "CellEditor", componentId: "sc-ee08kh" }) `
15601
15657
  position: relative;
15602
15658
  min-height: 2rem;
15603
15659
  `;
15604
- const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-1i1yvb2" }) `
15660
+ const CellGhost = styled.span.withConfig({ displayName: "CellGhost", componentId: "sc-1p10qm6" }) `
15605
15661
  display: block;
15606
15662
  padding: 0.375rem 0.25rem;
15607
15663
  /* Рамка повторяет CellButton — с ней высота ячейки в правке совпадает с высотой на чтении. */
15608
15664
  border: 1px solid transparent;
15609
15665
  line-height: 1.25rem;
15610
- white-space: nowrap;
15611
15666
  visibility: hidden;
15667
+
15668
+ ${cellWrapMixin};
15612
15669
  `;
15613
- const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-1ww3875" }) `
15670
+ const CellField = styled.div.withConfig({ displayName: "CellField", componentId: "sc-cnkcp6" }) `
15614
15671
  position: absolute;
15615
15672
  inset: 0;
15616
15673
  display: flex;
@@ -15625,11 +15682,97 @@ const CellField = styled.div.withConfig({ displayName: "CellField", componentId:
15625
15682
  min-width: 0;
15626
15683
  }
15627
15684
  `;
15628
- const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-1vamijg" }) `
15685
+ /**
15686
+ * Ячейка со вложениями. Список идёт колонкой, кнопка добавления — под ним, поэтому строка растёт
15687
+ * ровно на столько, сколько файлов в ней лежит.
15688
+ *
15689
+ * Отступы те же, что у текстовой ячейки (`CellText`): в одном ряду с обычными колонками значения
15690
+ * обязаны стоять на одной линии.
15691
+ */
15692
+ const AttachmentsCellBox = styled.div.withConfig({ displayName: "AttachmentsCellBox", componentId: "sc-9n5afp" }) `
15693
+ display: flex;
15694
+ flex-direction: column;
15695
+ align-items: flex-start;
15696
+ gap: 0.25rem;
15697
+ padding: 0.375rem 0.25rem;
15698
+ `;
15699
+ const TableEmpty = styled.div.withConfig({ displayName: "TableEmpty", componentId: "sc-1c196zb" }) `
15629
15700
  padding: 0.75rem 0.25rem;
15630
15701
  color: ${({ theme }) => theme.palette.textSecondary};
15631
15702
  `;
15632
15703
 
15704
+ /**
15705
+ * Ячейка с вложениями: разбор значения, просмотр, скачивание и правка списка.
15706
+ *
15707
+ * Значение атрибута — строка: файлы приезжают в ней списком JSON, поэтому и обратно в черновик
15708
+ * кладём строку, а не массив — иначе тип ячейки менялся бы от самой правки.
15709
+ *
15710
+ * Добавление здесь только по ссылке. Загрузка с диска требует ресурса-родителя и файлового
15711
+ * api контейнера вложений, а у колонки таблицы ни того, ни другого нет.
15712
+ */
15713
+ const useAttachmentsCell = ({ attribute, row, canEdit, onChange }) => {
15714
+ const [previewIndex, setPreviewIndex] = React.useState(null);
15715
+ const [isLinkDialogOpen, setLinkDialogOpen] = React.useState(false);
15716
+ const { attributeName, isEditable } = attribute;
15717
+ const value = row.properties[attributeName];
15718
+ const items = React.useMemo(() => parseAttachments(value), [value]);
15719
+ const previewImages = useAttachmentPreviewImages({ items, active: previewIndex !== null });
15720
+ const downloadByIndex = useAttachmentDownload(items);
15721
+ // Те же два условия, что и у обычной ячейки: режим таблицы и разрешение самого атрибута.
15722
+ const editable = canEdit && isEditable;
15723
+ const persist = React.useCallback((next) => onChange(row.key, attributeName, JSON.stringify(next)), [attributeName, onChange, row.key]);
15724
+ const onPreview = React.useCallback((link) => {
15725
+ const index = items.findIndex(item => item.link === link);
15726
+ if (index >= 0) {
15727
+ setPreviewIndex(index);
15728
+ }
15729
+ }, [items]);
15730
+ const onClosePreview = React.useCallback(() => setPreviewIndex(null), []);
15731
+ const onDownload = React.useCallback((_image, index) => downloadByIndex(index), [downloadByIndex]);
15732
+ const onDelete = React.useCallback((link) => persist(items.filter(item => item.link !== link)), [items, persist]);
15733
+ const onOpenLinkDialog = React.useCallback(() => setLinkDialogOpen(true), []);
15734
+ const onCloseLinkDialog = React.useCallback(() => setLinkDialogOpen(false), []);
15735
+ const onAddByLink = React.useCallback((url) => persist([
15736
+ ...items,
15737
+ {
15738
+ link: url,
15739
+ name: getFileNameFromUrl(url),
15740
+ mimeType: getMimeTypeFromUrl(url),
15741
+ date: new Date().toISOString(),
15742
+ isExternal: true,
15743
+ },
15744
+ ]), [items, persist]);
15745
+ return {
15746
+ items,
15747
+ editable,
15748
+ previewIndex,
15749
+ previewImages,
15750
+ isLinkDialogOpen,
15751
+ onPreview,
15752
+ onClosePreview,
15753
+ onDownload,
15754
+ onDelete,
15755
+ onOpenLinkDialog,
15756
+ onCloseLinkDialog,
15757
+ onAddByLink,
15758
+ };
15759
+ };
15760
+
15761
+ /**
15762
+ * Колонка со вложениями: список файлов прямо в ячейке.
15763
+ *
15764
+ * Тип у атрибута строковый, вложениями его делает `subType: "Attachments"` в схеме контейнера —
15765
+ * поэтому ветка стоит раньше разбора типа и обычных редакторов ячейки.
15766
+ */
15767
+ const AttachmentsCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15768
+ const { t } = useGlobalContext();
15769
+ const { items, editable, previewIndex, previewImages, isLinkDialogOpen, onPreview, onClosePreview, onDownload, onDelete, onOpenLinkDialog, onCloseLinkDialog, onAddByLink, } = useAttachmentsCell({ attribute, row, canEdit, onChange });
15770
+ return (jsxRuntime.jsxs(AttachmentsCellBox, { children: [!items.length && !editable && jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }), jsxRuntime.jsx(AttachmentsList, { items: items, isEdit: editable, onPreview: onPreview, onDelete: onDelete }), editable && (jsxRuntime.jsx(uilibGl.IconButton, { kind: "link", tabIndex: 0, title: t("attachments.fromLink", { ns: "common", defaultValue: "По ссылке" }), onClick: onOpenLinkDialog })), jsxRuntime.jsx(AttachmentLinkDialog, { isOpen: isLinkDialogOpen, onClose: onCloseLinkDialog, onSubmit: onAddByLink }), previewIndex !== null && (jsxRuntime.jsx(uilibGl.Preview, { images: previewImages, initialIndex: previewIndex, isOpen: true, onClose: onClosePreview, onDownload: onDownload, errorTitleText: t("attachments.resourceUnavailable", {
15771
+ ns: "common",
15772
+ defaultValue: "Ресурс недоступен",
15773
+ }) }, previewIndex))] }));
15774
+ });
15775
+
15633
15776
  /**
15634
15777
  * Значение ячейки не изменилось.
15635
15778
  *
@@ -15728,7 +15871,7 @@ const useCellEditing = () => {
15728
15871
  const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15729
15872
  const { t, language } = useGlobalContext();
15730
15873
  const { editing, buttonProps, editorProps } = useCellEditing();
15731
- const { attributeName, type, isEditable, stringFormat } = attribute;
15874
+ const { attributeName, type, subType, isEditable, stringFormat, multiline } = attribute;
15732
15875
  const value = row.properties[attributeName];
15733
15876
  const handleChange = React.useCallback((next) => {
15734
15877
  // Пустые правки отбрасываем: иначе строка становилась бы «изменённой» от простого
@@ -15747,16 +15890,21 @@ const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15747
15890
  const withTime = React.useMemo(() => TIME_FORMAT.test(stringFormat?.format ?? ""), [stringFormat?.format]);
15748
15891
  // `canEdit` — режим таблицы, `isEditable` — разрешение конкретного атрибута: правка требует обоих.
15749
15892
  const editable = canEdit && isEditable && EDITABLE_ATTRIBUTE_TYPES.includes(type);
15893
+ // Вложения — это строка с JSON, а не текст: разбирать и рисовать её нужно раньше любых
15894
+ // редакторов по типу, иначе в ячейке оказался бы сырой список файлов.
15895
+ if (subType === api.StringSubType.Attachments) {
15896
+ return jsxRuntime.jsx(AttachmentsCell, { attribute: attribute, row: row, canEdit: canEdit, onChange: onChange });
15897
+ }
15750
15898
  // Логическое значение и на чтение показываем галкой, а не словами «true»/«false» —
15751
15899
  // так же, как остальной дашборд рисует булевы атрибуты.
15752
15900
  if (type === api.AttributeType.Boolean) {
15753
15901
  return jsxRuntime.jsx(uilibGl.Checkbox, { checked: !!value, disabled: !editable, onChange: () => handleChange(!value) });
15754
15902
  }
15755
15903
  if (!editable) {
15756
- return jsxRuntime.jsx(CellText, { title: formatted, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) });
15904
+ return (jsxRuntime.jsx(CellText, { "$multiline": multiline, title: formatted, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
15757
15905
  }
15758
15906
  if (!editing) {
15759
- return (jsxRuntime.jsx(CellButton, { type: "button", ...buttonProps, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
15907
+ return (jsxRuntime.jsx(CellButton, { type: "button", "$multiline": multiline, ...buttonProps, children: formatted || jsxRuntime.jsx(CellPlaceholder, { children: "\u2014" }) }));
15760
15908
  }
15761
15909
  const renderEditor = () => {
15762
15910
  if (type === api.AttributeType.DateTime) {
@@ -15767,9 +15915,31 @@ const TableCell = React.memo(({ attribute, row, canEdit, onChange }) => {
15767
15915
  }
15768
15916
  return (jsxRuntime.jsx(uilibGl.Input, { autoFocus: true, value: value === null || value === undefined ? "" : String(value), width: "100%", onChange: ({ target }) => handleChange(target.value) }));
15769
15917
  };
15770
- return (jsxRuntime.jsxs(CellEditor, { ...editorProps, children: [jsxRuntime.jsx(CellGhost, { children: formatted || "—" }), jsxRuntime.jsx(CellField, { children: renderEditor() })] }));
15918
+ return (jsxRuntime.jsxs(CellEditor, { ...editorProps, children: [jsxRuntime.jsx(CellGhost, { "$multiline": multiline, children: formatted || "—" }), jsxRuntime.jsx(CellField, { children: renderEditor() })] }));
15771
15919
  });
15772
15920
 
15921
+ /**
15922
+ * Инлайн-размер ячейки колонки с заданной шириной.
15923
+ *
15924
+ * Раскладка таблицы автоматическая (ширину колонок выбирает содержимое), и одной `width` ей мало —
15925
+ * она для неё лишь пожелание. Жёстко колонку держат все три предела сразу, поэтому и ставим их
15926
+ * втроём, на каждую ячейку колонки.
15927
+ *
15928
+ * Ширины нет — стиля нет: колонка остаётся на попечении раскладки, как и была.
15929
+ */
15930
+ const getColumnStyle = (width) => width == null ? undefined : { width, minWidth: width, maxWidth: width };
15931
+
15932
+ /**
15933
+ * Шапка таблицы: заголовки колонок, сортировка кликом и захват границы для ручной ширины.
15934
+ *
15935
+ * Захват рисуется по разрешению атрибута (`resizable`) и не смотрит на режим правки контейнера:
15936
+ * ширина колонки — это вид, а не данные.
15937
+ */
15938
+ const TableHeadRow = ({ columns, sort, sortEnabled, columnWidths, withActionsColumn, onSortToggle, onResizeStart, onResizeMove, onResizeEnd, }) => (jsxRuntime.jsxs("tr", { children: [columns.map(({ attributeName, alias, description, resizable }) => {
15939
+ const sorted = sort?.attributeName === attributeName;
15940
+ return (jsxRuntime.jsxs(TableHeadCell, { title: description || alias, style: getColumnStyle(columnWidths[attributeName]), "$sortable": sortEnabled, onClick: sortEnabled ? () => onSortToggle(attributeName) : undefined, children: [jsxRuntime.jsxs(TableHeadContent, { children: [alias, sortEnabled && (jsxRuntime.jsx(TableSortSlot, { "$active": sorted, children: jsxRuntime.jsx(uilibGl.Icon, { kind: sorted && sort?.direction === "desc" ? "sorting_des" : "sorting_asc" }) }))] }), resizable && (jsxRuntime.jsx(TableResizeHandle, { onPointerDown: event => onResizeStart(attributeName, event), onPointerMove: onResizeMove, onPointerUp: onResizeEnd, onPointerCancel: onResizeEnd }))] }, attributeName));
15941
+ }), withActionsColumn && jsxRuntime.jsx(TableHeadCell, {})] }));
15942
+
15773
15943
  const ContainerLoading = () => (jsxRuntime.jsx(uilibGl.Flex, { alignContent: "center", justifyContent: "center", width: "100%", children: jsxRuntime.jsx(uilibGl.CircularProgress, { diameter: 1.5, mono: true }) }));
15774
15944
 
15775
15945
  /**
@@ -15873,6 +16043,53 @@ const getTableBoxStyle = (width, height) => {
15873
16043
  };
15874
16044
  };
15875
16045
 
16046
+ /**
16047
+ * Ручное изменение ширины колонок.
16048
+ *
16049
+ * Растянутые ширины живут только здесь: тянут их в рантайме, а `width` атрибута в конфиге задаёт
16050
+ * лишь стартовое значение — записывать растянутое обратно в конфиг незачем.
16051
+ *
16052
+ * Разрешение (`resizable`) от `options.editMode` не зависит: ширина колонки — это вид таблицы,
16053
+ * а не правка данных, и тянуть её должно быть можно и на таблице только для чтения.
16054
+ *
16055
+ * Стартовую ширину берём с самой ячейки, а не из конфига: у колонки без `width` её выбрала
16056
+ * раскладка таблицы, и тянуть надо ровно от того, что видно.
16057
+ */
16058
+ const useColumnResize = (columns) => {
16059
+ const [widths, setWidths] = React.useState({});
16060
+ const dragRef = React.useRef(null);
16061
+ const onResizeStart = React.useCallback((attributeName, event) => {
16062
+ const handle = event.currentTarget;
16063
+ // Захват границы — не клик по заголовку: иначе каждое перетаскивание переключало бы сортировку.
16064
+ event.preventDefault();
16065
+ event.stopPropagation();
16066
+ dragRef.current = {
16067
+ attributeName,
16068
+ startX: event.clientX,
16069
+ startWidth: handle.closest("th")?.getBoundingClientRect().width ?? MIN_COLUMN_WIDTH,
16070
+ };
16071
+ handle.setPointerCapture(event.pointerId);
16072
+ }, []);
16073
+ const onResizeMove = React.useCallback((event) => {
16074
+ const drag = dragRef.current;
16075
+ if (!drag) {
16076
+ return;
16077
+ }
16078
+ const next = Math.max(MIN_COLUMN_WIDTH, Math.round(drag.startWidth + event.clientX - drag.startX));
16079
+ setWidths(current => current[drag.attributeName] === next ? current : { ...current, [drag.attributeName]: next });
16080
+ }, []);
16081
+ const onResizeEnd = React.useCallback((event) => {
16082
+ const handle = event.currentTarget;
16083
+ dragRef.current = null;
16084
+ if (handle.hasPointerCapture(event.pointerId)) {
16085
+ handle.releasePointerCapture(event.pointerId);
16086
+ }
16087
+ }, []);
16088
+ // Растянутая ширина перебивает конфигурационную: последнее слово за тем, кто тянул.
16089
+ const columnWidths = React.useMemo(() => columns.reduce((acc, { attributeName, width }) => ({ ...acc, [attributeName]: widths[attributeName] ?? width }), {}), [columns, widths]);
16090
+ return { columnWidths, onResizeStart, onResizeMove, onResizeEnd };
16091
+ };
16092
+
15876
16093
  /**
15877
16094
  * Вид таблицы: колонки плюс локальная сортировка.
15878
16095
  *
@@ -15885,6 +16102,9 @@ const getTableBoxStyle = (width, height) => {
15885
16102
  *
15886
16103
  * `width`/`height` — размер собственного бокса таблицы: он ограничивает её саму и прокручивает
15887
16104
  * то, что не влезло. Место, отведённое контейнеру, задаётся его же опциями и живёт отдельно.
16105
+ *
16106
+ * Ширины колонок — тоже вид: конфиг задаёт стартовую, растянутая мышью живёт в
16107
+ * {@link useColumnResize} и в конфиг не возвращается.
15888
16108
  */
15889
16109
  const useTableView = (elementConfig) => {
15890
16110
  const context = React.useContext(StructuredDataContext);
@@ -15895,6 +16115,7 @@ const useTableView = (elementConfig) => {
15895
16115
  const columns = React.useMemo(() => schema ?? [], [schema]);
15896
16116
  const sortedRows = React.useMemo(() => sortRows(rows ?? [], sort, schema ?? []), [rows, schema, sort]);
15897
16117
  const sizeCss = React.useMemo(() => getTableBoxStyle(width, height), [height, width]);
16118
+ const { columnWidths, onResizeStart, onResizeMove, onResizeEnd } = useColumnResize(columns);
15898
16119
  const onSortToggle = React.useCallback((attributeName) => setSort(current => getNextSort(current, attributeName)), []);
15899
16120
  return {
15900
16121
  context,
@@ -15905,7 +16126,11 @@ const useTableView = (elementConfig) => {
15905
16126
  sizeCss,
15906
16127
  // Ширина задана — колонки по содержимому: лишнее уходит в прокрутку бокса, а не в многоточие.
15907
16128
  contentWidth: width != null,
16129
+ columnWidths,
15908
16130
  onSortToggle,
16131
+ onResizeStart,
16132
+ onResizeMove,
16133
+ onResizeEnd,
15909
16134
  };
15910
16135
  };
15911
16136
 
@@ -15917,7 +16142,7 @@ const useTableView = (elementConfig) => {
15917
16142
  */
15918
16143
  const ElementTable = React.memo(({ elementConfig }) => {
15919
16144
  const { t } = useGlobalContext();
15920
- const { context, columns, rows, sort, sortEnabled, sizeCss, contentWidth, onSortToggle } = useTableView(elementConfig);
16145
+ const { context, columns, rows, sort, sortEnabled, sizeCss, contentWidth, columnWidths, onSortToggle, onResizeStart, onResizeMove, onResizeEnd, } = useTableView(elementConfig);
15921
16146
  // Узел таблицы держим состоянием, а не ref: отсечка должна встать сразу, как он появится.
15922
16147
  const [table, setTable] = React.useState(null);
15923
16148
  useHeadOverlap(table);
@@ -15933,10 +16158,10 @@ const ElementTable = React.memo(({ elementConfig }) => {
15933
16158
  defaultValue: "Схема данных не задана",
15934
16159
  }) }));
15935
16160
  }
15936
- return (jsxRuntime.jsx(TableBox, { "$sizeCss": sizeCss, children: jsxRuntime.jsxs(TableWrapper, { ref: setTable, "$contentWidth": contentWidth, children: [jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsxs("tr", { children: [columns.map(({ attributeName, alias, description }) => {
15937
- const sorted = sort?.attributeName === attributeName;
15938
- return (jsxRuntime.jsx(TableHeadCell, { title: description || alias, "$sortable": sortEnabled, onClick: sortEnabled ? () => onSortToggle(attributeName) : undefined, children: jsxRuntime.jsxs(TableHeadContent, { children: [alias, sortEnabled && (jsxRuntime.jsx(TableSortSlot, { "$active": sorted, children: jsxRuntime.jsx(uilibGl.Icon, { kind: sorted && sort?.direction === "desc" ? "sorting_des" : "sorting_asc" }) }))] }) }, attributeName));
15939
- }), context.canDelete && jsxRuntime.jsx(TableHeadCell, {})] }) }), jsxRuntime.jsx(TableBody, { children: rows.map(row => (jsxRuntime.jsxs(TableRow, { children: [columns.map(attribute => (jsxRuntime.jsx(TableCellWrapper, { children: jsxRuntime.jsx(TableCell, { attribute: attribute, row: row, canEdit: context.canEdit, onChange: context.onCellChange }) }, attribute.attributeName))), context.canDelete && (jsxRuntime.jsx(TableActionsCell, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", title: t("actions.delete", { ns: "common", defaultValue: "Удалить" }), onClick: () => context.onRowDelete(row.key) }) }))] }, row.key))) })] }) }));
16161
+ return (jsxRuntime.jsx(TableBox, { "$sizeCss": sizeCss, children: jsxRuntime.jsxs(TableWrapper, { ref: setTable, "$contentWidth": contentWidth, children: [jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsx(TableHeadRow, { columns: columns, sort: sort, sortEnabled: sortEnabled, columnWidths: columnWidths, withActionsColumn: context.canDelete, onSortToggle: onSortToggle, onResizeStart: onResizeStart, onResizeMove: onResizeMove, onResizeEnd: onResizeEnd }) }), jsxRuntime.jsx(TableBody, { children: rows.map(row => (jsxRuntime.jsxs(TableRow, { children: [columns.map(attribute => (jsxRuntime.jsx(TableCellWrapper, {
16162
+ // Стиль колонки из конфига идёт последним: шрифт задан для значений, и ширину
16163
+ // он не трогает своих размеров у него нет.
16164
+ style: { ...getColumnStyle(columnWidths[attribute.attributeName]), ...attribute.style }, children: jsxRuntime.jsx(TableCell, { attribute: attribute, row: row, canEdit: context.canEdit, onChange: context.onCellChange }) }, attribute.attributeName))), context.canDelete && (jsxRuntime.jsx(TableActionsCell, { children: jsxRuntime.jsx(uilibGl.IconButton, { kind: "delete", title: t("actions.delete", { ns: "common", defaultValue: "Удалить" }), onClick: () => context.onRowDelete(row.key) }) }))] }, row.key))) })] }) }));
15940
16165
  });
15941
16166
 
15942
16167
  const TooltipIcon = styled(uilibGl.Icon).withConfig({ displayName: "TooltipIcon", componentId: "sc-1lkxudm" }) `