@eocrm/design-system 0.6.20 → 0.6.21

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.
@@ -53,7 +53,7 @@ const instance = useDataTable<Deal>({
53
53
  - Resize via the right-edge handle. Without `collapseBelow`, the legacy keyboard path stays on a focused sortable header label: `←`/`→` for −/+8px; Shift+`←`/`→` for ±32px. With `collapseBelow`, the wide presentation exposes a dedicated focusable resize separator with the same keys; the separator and resize interaction are unavailable while stacked and return after widening.
54
54
  - `ColumnVisibilityTrigger` is the only built-in companion. For column pinning UI (Phase 2 ships state, no built-in UI), wire your own using `instance.pinColumn(id, side)`.
55
55
  - **Phase 2 ships pinning rendering.** `columnPinning` now applies sticky CSS with cumulative offsets and an inside-edge shadow; `pinnedRows` renders in a separate `<tbody>` above the main body. The selection auto-column is auto-left-pinned at offset 0. Pinned columns are LOCKED in place — no drag grip, can't be reordered. Their sort still works. Declare initial pinning either on the hook (`defaultColumnPinning: { left: ['name'], right: ['actions'] }`) or directly on each column (`pin: 'left' | 'right'`); the hook prop wins when both are set. **Phase 3 ships expandable rows.** Pass `renderExpandedRow: (row) => ReactNode` to add a per-row chevron auto-column at the left edge. Clicking the chevron toggles a detail row beneath the main row, spanning all columns. ARIA-wired with `aria-expanded` + `aria-controls`. The chevron auto-column is sticky-left like the selection cell; when both are enabled, selection is first and expand is second. DataTable is now feature-complete per the original spec.
56
- - **Cell content is single-line + ellipsized by default.** DataTable's internal table uses `table-layout: fixed` so column widths from `ColumnDef.size` (or runtime `columnSizing`) are authoritative, and every cell gets `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`. Wide content truncates with `…` at the column boundary; the column does NOT expand to fit. If you need multi-line cells, render `<Table>` directly — DataTable is opinionated here so pinning offsets stay pixel-correct.
56
+ - **Cell content is single-line + ellipsized by default.** DataTable's internal table uses `table-layout: fixed` so column widths from `ColumnDef.size` (or runtime `columnSizing`) are authoritative, and every cell gets `overflow: hidden; text-overflow: ellipsis; white-space: nowrap`. The table is as wide as the sum of its column sizes, or 100% of its container when that is wider (columns then scale up proportionally). Content never widens it. Wide content, including nested block content such as a `Stack` or a long `Badge`, is clipped at the column boundary, and text truncates with `…`; the column does NOT expand to fit. If you need multi-line cells, render `<Table>` directly — DataTable is opinionated here so pinning offsets stay pixel-correct.
57
57
  - **A column drag commits wherever the preview parked it** — however far the pointer roams, including over a pinned column or clear off the table. There is no "outside" to release into: the column is clamped to the unpinned band, so the preview is always showing a real slot and the drop honors it. This deliberately differs from `Kanban`, where releasing outside the columns cancels — there the pointer can genuinely aim somewhere else. **Escape is the only cancel.** The `dragWholeColumn={false}` opt-out is the exception: its preview is dnd-kit's own and RETRACTS (the header snaps home) when no unpinned slot is under the column, so a release there is discarded — matching what that preview shows.
58
58
 
59
59
  **When NOT to use:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eocrm/design-system",
3
- "version": "0.6.20",
3
+ "version": "0.6.21",
4
4
  "description": "Design system for the EOCRM product — React 19 + SCSS modules + CSS custom properties.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -50,7 +50,7 @@
50
50
  "@dnd-kit/core": "^6.3.1",
51
51
  "@dnd-kit/sortable": "^10.0.0",
52
52
  "@dnd-kit/utilities": "^3.2.2",
53
- "@eocrm/design-tokens": "0.6.20",
53
+ "@eocrm/design-tokens": "0.6.21",
54
54
  "@floating-ui/react-dom": "^2.1.8",
55
55
  "@types/d3-scale": "^4.0.9",
56
56
  "@types/d3-shape": "^3.2.0",
@@ -9,18 +9,18 @@
9
9
  // internal table-sizing properties (not consumer layout). Same precedent as the
10
10
  // Table primitive's `position: sticky` for the sticky-header band.
11
11
  .root {
12
- // table-layout: fixed + width: max-content makes each <col>'s declared
13
- // width AUTHORITATIVE for the cell. Default `table-layout: auto` plus the
14
- // Table primitive's `min-width: 100%` would distribute columns
15
- // proportionally within the parent's width, which causes pin offsets to
16
- // misalign — the 44px auto-cell rendering at e.g. 40px because the table
17
- // was capped at the wrap's width, then the left-pinned column at left:44
18
- // leaves a 4px gap that exposes scroll content during horizontal scroll.
19
- // `width: max-content` lets the table grow to its intrinsic column-sum
20
- // width when that exceeds the wrap; Table primitive's scrollWrap then
21
- // scrolls horizontally as designed.
12
+ // table-layout: fixed + an explicit width makes each <col>'s declared
13
+ // width AUTHORITATIVE for the cell. `--data-table-width` is the sum of the
14
+ // <col> widths, set inline by DataTable.tsx. Default `table-layout: auto`
15
+ // would distribute columns proportionally within the parent's width,
16
+ // misaligning pin offsets. Never use an intrinsic width (`max-content`)
17
+ // here: it is computed from cell content, so one long nowrap cell widens
18
+ // its column past `size` (#733). `min-width: 100%` still stretches the
19
+ // table to fill a wider wrap; a narrower wrap scrolls horizontally.
20
+ // Fallback `0` is still content-independent: a fixed-layout table is the
21
+ // greater of its `width` and the sum of its column widths.
22
22
  table-layout: fixed;
23
- width: max-content;
23
+ width: var(--data-table-width, 0);
24
24
  min-width: 100%;
25
25
  }
26
26
  // stylelint-enable property-disallowed-list
@@ -5,6 +5,7 @@ import {
5
5
  useMemo,
6
6
  useRef,
7
7
  useState,
8
+ type CSSProperties,
8
9
  type ReactNode,
9
10
  type Ref,
10
11
  type RefObject,
@@ -41,7 +42,7 @@ import {
41
42
  useDragAccessibility,
42
43
  type DescribeDragTarget,
43
44
  } from '../_internal/dragAnnouncements';
44
- import { AUTO_CELL_WIDTH } from './pinStyle';
45
+ import { AUTO_CELL_WIDTH, DEFAULT_COL_WIDTH } from './pinStyle';
45
46
  import type { DataTableInstance } from './types';
46
47
  import type { CollapseBreakpoint } from '../_internal/collapse';
47
48
  import { useTranslation } from '../../i18n/useTranslation';
@@ -380,6 +381,16 @@ function DataTableInner<T>(
380
381
  [instance.leftPinnedColumns, instance.unpinnedColumns, instance.rightPinnedColumns],
381
382
  );
382
383
 
384
+ // Explicit table width = sum of the <col> widths. Under `table-layout:
385
+ // fixed` that keeps every column at its declared size (#733); an intrinsic
386
+ // width like `max-content` would be computed from cell content instead.
387
+ const tableWidthPx =
388
+ ((instance.enableRowSelection ? 1 : 0) + (instance.hasExpansion ? 1 : 0)) * AUTO_CELL_WIDTH +
389
+ renderColumns.reduce(
390
+ (sum, col) => sum + (instance.columnSizesPx[col.id] ?? DEFAULT_COL_WIDTH),
391
+ 0,
392
+ );
393
+
383
394
  const totalColCount =
384
395
  instance.visibleColumns.length +
385
396
  (instance.enableRowSelection ? 1 : 0) +
@@ -542,6 +553,7 @@ function DataTableInner<T>(
542
553
  aria-rowcount={instance.rowCount}
543
554
  className={clsx(styles.root, className)}
544
555
  {...rest}
556
+ style={{ '--data-table-width': `${tableWidthPx}px` } as CSSProperties}
545
557
  scroll={!responsiveEnabled}
546
558
  >
547
559
  {caption && <Table.Caption>{caption}</Table.Caption>}
@@ -550,7 +562,10 @@ function DataTableInner<T>(
550
562
  {instance.enableRowSelection && <col style={{ width: AUTO_CELL_WIDTH }} />}
551
563
  {instance.hasExpansion && <col style={{ width: AUTO_CELL_WIDTH }} />}
552
564
  {renderColumns.map((col) => (
553
- <col key={col.id} style={{ width: instance.columnSizesPx[col.id] ?? 120 }} />
565
+ <col
566
+ key={col.id}
567
+ style={{ width: instance.columnSizesPx[col.id] ?? DEFAULT_COL_WIDTH }}
568
+ />
554
569
  ))}
555
570
  </colgroup>
556
571
 
@@ -15,7 +15,7 @@ import type { TableSortDirection } from '../Table';
15
15
  import { useTranslation } from '../../i18n/useTranslation';
16
16
  import { useResizeHandle } from './useResizeHandle';
17
17
  import type { ColumnDef, DataTableInstance } from './types';
18
- import { getPinStyle } from './pinStyle';
18
+ import { DEFAULT_COL_WIDTH, getPinStyle } from './pinStyle';
19
19
  import { shiftVarName } from './columnShift';
20
20
 
21
21
  import styles from './HeaderCell.module.scss';
@@ -223,7 +223,7 @@ export function HeaderCell<T>({
223
223
  });
224
224
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = sortableResult;
225
225
 
226
- const width = instance.columnSizesPx[column.id] ?? 120;
226
+ const width = instance.columnSizesPx[column.id] ?? DEFAULT_COL_WIDTH;
227
227
  const resize = useResizeHandle({
228
228
  initialWidth: width,
229
229
  minWidth: column.minSize ?? 40,
@@ -248,7 +248,7 @@ export function HeaderCell<T>({
248
248
  if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
249
249
  e.preventDefault();
250
250
  const step = (e.shiftKey ? 32 : 8) * (e.key === 'ArrowRight' ? 1 : -1);
251
- const current = instance.columnSizesPx[column.id] ?? 120;
251
+ const current = instance.columnSizesPx[column.id] ?? DEFAULT_COL_WIDTH;
252
252
  const minW = column.minSize ?? 40;
253
253
  const maxW = column.maxSize;
254
254
  let next = current + step;
@@ -6,6 +6,9 @@ import type { DataTableInstance } from './types';
6
6
  */
7
7
  export const AUTO_CELL_WIDTH = 44;
8
8
 
9
+ /** Width of a column with no `size` and no runtime sizing state. */
10
+ export const DEFAULT_COL_WIDTH = 120;
11
+
9
12
  export interface PinStyle {
10
13
  position?: 'sticky';
11
14
  left?: number;
@@ -7,9 +7,10 @@ import { clampX, computeColumnShifts, shiftVarName, type DragRangeX } from './co
7
7
  * geometry of the header cells inside `root`.
8
8
  *
9
9
  * Declared widths (`columnSizingPx` / the `<col>` elements) cannot be used for
10
- * this. `DataTable.module.scss` sets `table-layout: fixed; width: max-content;
11
- * min-width: 100%`, so whenever the column sum is narrower than the scroll wrap
12
- * the table stretches and every rendered column is WIDER than declared. Summing
10
+ * this. `DataTable.module.scss` sets `table-layout: fixed;
11
+ * width: var(--data-table-width, 0); min-width: 100%`, so whenever the column
12
+ * sum is narrower than the scroll wrap the table stretches and every rendered
13
+ * column is WIDER than declared. Summing
13
14
  * declared widths then under-reports the travel and makes the last slot
14
15
  * unreachable — the exact regression this replaced.
15
16
  *
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
  import { useControllableState } from '../_internal/useControllableState';
3
+ import { DEFAULT_COL_WIDTH } from './pinStyle';
3
4
  import type {
4
5
  ColumnOrderState,
5
6
  ColumnPinningState,
@@ -157,8 +158,6 @@ export function useDataTable<T>(options: UseDataTableOptions<T>): DataTableInsta
157
158
  onChange: options.onSortChange,
158
159
  });
159
160
 
160
- const DEFAULT_COL_WIDTH = 120;
161
-
162
161
  const columnsById = useMemo(() => {
163
162
  const m = new Map<string, (typeof columns)[number]>();
164
163
  for (const c of columns) m.set(c.id, c);