@eocrm/design-system 0.6.20 → 0.6.22
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/AI-PRIMER.md +1 -1
- package/README.md +17 -4
- package/docs/components/DataTable.md +1 -1
- package/docs/components/Link.md +2 -2
- package/docs/setup.md +1 -1
- package/package.json +2 -2
- package/src/components/DataTable/DataTable.module.scss +11 -11
- package/src/components/DataTable/DataTable.tsx +17 -2
- package/src/components/DataTable/HeaderCell.tsx +3 -3
- package/src/components/DataTable/pinStyle.ts +3 -0
- package/src/components/DataTable/useColumnDragShift.ts +4 -3
- package/src/components/DataTable/useDataTable.ts +1 -2
- package/src/components/Link/Link.tokens.scss +4 -3
- package/src/components/Link/Link.tsx +1 -1
package/AI-PRIMER.md
CHANGED
|
@@ -20,7 +20,7 @@ Read this first. Each component's contract is its `docs/components/<Name>.md`; t
|
|
|
20
20
|
|
|
21
21
|
See [docs/setup.md](docs/setup.md).
|
|
22
22
|
|
|
23
|
-
Accent: blue by default; `<html data-accent="
|
|
23
|
+
Accent: blue by default; `<html data-accent="…">` switches to `tealpine`, `indigo`, `grape`, `berry`, `slate`, `ocean` or `moss` (see [README.md](./README.md#accent)).
|
|
24
24
|
|
|
25
25
|
## Localization (i18n)
|
|
26
26
|
|
package/README.md
CHANGED
|
@@ -63,17 +63,30 @@ If you need only the tokens (you're providing your own reset), import `@eocrm/de
|
|
|
63
63
|
|
|
64
64
|
## Accent
|
|
65
65
|
|
|
66
|
-
The accent family defaults to blue. To switch the whole app to
|
|
66
|
+
The accent family defaults to blue. To switch the whole app to another accent, set `data-accent` on `<html>` (omit the attribute for blue):
|
|
67
67
|
|
|
68
68
|
```html
|
|
69
69
|
<html data-accent="tealpine" data-theme="dark"></html>
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
| `data-accent` | Hue | Compose objects |
|
|
73
|
+
| ------------- | ---------------- | ------------------------------------------------------ |
|
|
74
|
+
| _(none)_ | Blue (default) | `EocrmLightTokens` / `EocrmDarkTokens` |
|
|
75
|
+
| `tealpine` | Teal pine | `EocrmTealPineLightTokens` / `EocrmTealPineDarkTokens` |
|
|
76
|
+
| `indigo` | Indigo | `EocrmIndigoLightTokens` / `EocrmIndigoDarkTokens` |
|
|
77
|
+
| `grape` | Purple | `EocrmGrapeLightTokens` / `EocrmGrapeDarkTokens` |
|
|
78
|
+
| `berry` | Magenta-pink | `EocrmBerryLightTokens` / `EocrmBerryDarkTokens` |
|
|
79
|
+
| `slate` | Neutral graphite | `EocrmSlateLightTokens` / `EocrmSlateDarkTokens` |
|
|
80
|
+
| `ocean` | Cyan-blue | `EocrmOceanLightTokens` / `EocrmOceanDarkTokens` |
|
|
81
|
+
| `moss` | Olive green | `EocrmMossLightTokens` / `EocrmMossDarkTokens` |
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
Each accent swaps `--color-accent`, `-hover`, `-pressed`, `-fg`, `-bg-subtle`, `-subtle-bg` and `--ring-accent`, plus the default `Link` colour (`--color-link-fg` / `-hover`), in light, forced dark (`data-theme="dark"`) and OS dark. They ship in the existing `tokens.scss` / `dark.scss`, so there is no new import. `AppProvider`'s `tokens` / `darkTokens` overrides win over `data-accent` in every theme (see [docs/setup.md](docs/setup.md)). Info colours stay blue under any accent; DropdownMenu checked items, the OptionsPicker selected row and StagePath done states follow the accent. In light theme, Teal pine's default `Link`s use a slightly brighter teal than the accent (`#007676`, AA on every grey surface) at medium weight (not inside headings, `<strong>` or `<th>`), because the accent alone sits too close to the navy body text to read as a link. Slate's default `Link`s are medium weight for the same reason. Indigo and Berry also set a slightly different link colour from their accent (`#4342c4`, `#a82c68`) to keep links at AA on every grey surface.
|
|
75
84
|
|
|
76
|
-
|
|
85
|
+
Compose: each accent's Light / Dark objects sit beside `EocrmLightTokens` / `EocrmDarkTokens`; only accent colours differ. The link colour tokens are web-only (Compose has no `Link`), so Compose links keep the accent colour.
|
|
86
|
+
|
|
87
|
+
There is deliberately no red, orange or yellow accent: a primary button in those hues reads as a danger or warning action.
|
|
88
|
+
|
|
89
|
+
**Known issue:** in light theme, Teal pine and Moss sit close to `--color-success` in hue. Review success/accent pairings visually before switching the CRM to either.
|
|
77
90
|
|
|
78
91
|
---
|
|
79
92
|
|
|
@@ -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
|
|
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/docs/components/Link.md
CHANGED
|
@@ -40,7 +40,7 @@ import { Link as RouterLink } from 'react-router-dom';
|
|
|
40
40
|
- **Polymorphic**: `as={Component}` forwards all of Component's props with full TypeScript inference.
|
|
41
41
|
- **Library has no router dependency** — the `as` mechanism is consumer-driven.
|
|
42
42
|
- **Three variants**:
|
|
43
|
-
- `default`: link colour (`--color-link-fg`; the accent unless the accent set overrides it — `tealpine` uses a brighter teal at medium weight), hover-underline. Inline CTA ("View all →").
|
|
43
|
+
- `default`: link colour (`--color-link-fg`; the accent unless the accent set overrides it — `tealpine` uses a brighter teal at medium weight; `slate` links are medium weight), hover-underline. Inline CTA ("View all →").
|
|
44
44
|
- `muted`: muted color, hover-accent. Low-emphasis nav (breadcrumb-style).
|
|
45
45
|
- `subtle`: foreground color, hover-accent + underline. Dense-surface name links. **Not deprecated** — this is `Link`'s own variant and is unrelated to `Text`/`Title`'s `tone="subtle"`, which WAS deprecated. Same word, opposite status: a grep for "subtle deprecated" will land on the tone, and `PersonDisplay.Name` uses this variant deliberately.
|
|
46
46
|
- **`underline` prop** (`'hover'` | `'always'` | `'none'`, default `'hover'`) — orthogonal to `variant`:
|
|
@@ -62,4 +62,4 @@ import { Link as RouterLink } from 'react-router-dom';
|
|
|
62
62
|
- ❌ Forgetting `rel="noopener noreferrer"` on `target="_blank"` links.
|
|
63
63
|
- ❌ Using `variant="default"` for low-emphasis nav like breadcrumbs.
|
|
64
64
|
- ❌ A Link inside running text/body copy without `underline="always"` — color alone isn't a sufficient visual cue (WCAG 1.4.1).
|
|
65
|
-
- ❌ A `default` Link inside `<Text weight="semibold">` (or `bold`) under `data-accent="tealpine"` — the link renders at the accent's medium weight, lighter than the words around it. Wrap the bold run in `<strong>` or give the Link a matching weight via `className`.
|
|
65
|
+
- ❌ A `default` Link inside `<Text weight="semibold">` (or `bold`) under `data-accent="tealpine"` or `"slate"` — the link renders at the accent's medium weight, lighter than the words around it. Wrap the bold run in `<strong>` or give the Link a matching weight via `className`.
|
package/docs/setup.md
CHANGED
|
@@ -48,4 +48,4 @@ const darkTokens = useMemo(() => ({ '--color-accent': '#a78bfa' }), []);
|
|
|
48
48
|
</AppProvider>;
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`tokens` apply in light **and** dark; `darkTokens` refine the dark scope only. They're emitted as a single declarative `<style>` that layers over the [dark theme](theming.md#dark-theme) correctly, and they win over a `data-accent` preset's token blocks. One exception to "override the accent and everything follows": a preset may set the link colour apart from its accent (`tealpine`
|
|
51
|
+
`tokens` apply in light **and** dark; `darkTokens` refine the dark scope only. They're emitted as a single declarative `<style>` that layers over the [dark theme](theming.md#dark-theme) correctly, and they win over a `data-accent` preset's token blocks. One exception to "override the accent and everything follows": a preset may set the link colour apart from its accent (`tealpine`, `indigo` and `berry` do), so when rebranding the accent on top of a preset, also override `--color-link-fg` / `--color-link-fg-hover`. Any design token can be overridden — see the generated `@eocrm/design-tokens` Sass contract for the full CSS custom-property set. Contributors change shared values only in `packages/design-tokens/src/tokens.json`; `src/styles/tokens.scss` remains the stable consumer entry point.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eocrm/design-system",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.22",
|
|
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.
|
|
53
|
+
"@eocrm/design-tokens": "0.6.22",
|
|
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
|
|
13
|
-
// width AUTHORITATIVE for the cell.
|
|
14
|
-
//
|
|
15
|
-
// proportionally within the parent's width,
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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:
|
|
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
|
|
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] ??
|
|
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] ??
|
|
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;
|
|
11
|
-
* min-width: 100%`, so whenever the column
|
|
12
|
-
* the table stretches and every rendered
|
|
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);
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
--link-fg-subtle-hover: var(--color-accent);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// Teal pine's
|
|
25
|
-
// also step up in weight (the colour alone can't carry the difference).
|
|
26
|
-
:root[data-accent='tealpine']
|
|
24
|
+
// Teal pine's and Slate's accents sit close to the navy body text, so their
|
|
25
|
+
// default links also step up in weight (the colour alone can't carry the difference).
|
|
26
|
+
:root[data-accent='tealpine'],
|
|
27
|
+
:root[data-accent='slate'] {
|
|
27
28
|
--link-font-weight-default: var(--font-weight-medium);
|
|
28
29
|
}
|
|
@@ -14,7 +14,7 @@ import styles from './Link.module.scss';
|
|
|
14
14
|
* Visual variant. Defaults to `'default'`.
|
|
15
15
|
* - `'default'` — link color (`--color-link-fg`, the accent unless the accent set
|
|
16
16
|
* overrides it), no underline at rest, underline + hover color on hover. Under
|
|
17
|
-
* `data-accent="tealpine"` it is also medium weight, except inside headings,
|
|
17
|
+
* `data-accent="tealpine"` or `"slate"` it is also medium weight, except inside headings,
|
|
18
18
|
* `<strong>`/`<b>` and `<th>`.
|
|
19
19
|
* Use for inline CTA links ("View all →", "Read more").
|
|
20
20
|
* - `'muted'` — muted color, no underline, accent-hover on hover.
|