@crisp-ui-kit/crisp 0.31.1 → 0.31.3

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.
@@ -37,6 +37,7 @@ exports.DataTable = DataTable;
37
37
  const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const lucide_react_1 = require("lucide-react");
39
39
  const React = __importStar(require("react"));
40
+ const core_1 = require("../../core");
40
41
  const checkbox_1 = require("../checkbox");
41
42
  const menu_1 = require("../menu");
42
43
  const table_1 = require("../table");
@@ -365,10 +366,13 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
365
366
  nr = Math.max(0, r - 1);
366
367
  else if (e.key === "ArrowDown")
367
368
  nr = Math.min(rows.length - 1, r + 1);
368
- else if (e.key === "ArrowLeft")
369
- nc = Math.max(0, c - 1);
370
- else if (e.key === "ArrowRight")
371
- nc = Math.min(columns.length - 1, c + 1);
369
+ // The arrows follow what the user SEES: under RTL the first column renders
370
+ // rightmost, so ArrowLeft steps forward and ArrowRight steps back (#255).
371
+ else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
372
+ const rtl = (0, core_1.isRtlElement)(e.currentTarget);
373
+ const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
374
+ nc = forward ? Math.min(columns.length - 1, c + 1) : Math.max(0, c - 1);
375
+ }
372
376
  else if (e.key === "Escape") {
373
377
  setSelCell(null);
374
378
  return;
@@ -426,6 +430,11 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
426
430
  key: col.key,
427
431
  startX: e.clientX,
428
432
  startW: colWidth(col),
433
+ // The columns reverse under RTL, so the grip — anchored at
434
+ // `inset-inline-end` — lands on the column's LEFT edge there, and
435
+ // pulling it further left must GROW the column. `clientX` stays
436
+ // physical, so the sign is the other half of that conversion (#214).
437
+ dir: (0, core_1.isRtlElement)(e.currentTarget) ? -1 : 1,
429
438
  };
430
439
  setResizingKey(col.key);
431
440
  }
@@ -433,7 +442,7 @@ function DataTable({ columns: columnsProp, data, selectable = false, selectedIds
433
442
  const r = resizeRef.current;
434
443
  if (!r)
435
444
  return;
436
- const w = Math.max(64, r.startW + (e.clientX - r.startX));
445
+ const w = Math.max(64, r.startW + r.dir * (e.clientX - r.startX));
437
446
  setWidths((prev) => ({ ...prev, [r.key]: w }));
438
447
  }
439
448
  function endResize(e) {
@@ -73,8 +73,8 @@ const config = (0, core_1.defineRecipe)({
73
73
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
74
74
  & table { table-layout: fixed; }
75
75
  /* column resize grip */
76
- & .datatable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
77
- & .datatable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: ${tokens_1.contract.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
76
+ & .datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
77
+ & .datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: ${tokens_1.contract.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
78
78
  & .datatable-resize:hover::after, & .datatable-resize[data-resizing]::after { opacity: 1; }
79
79
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
80
80
  & .datatable-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; color: ${tokens_1.contract.fgPrimary}; font-weight: ${tokens_1.contract.weight.medium}; }
@@ -721,6 +721,36 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
721
721
  const setSortField = (i, key) => setSorts(sorts.map((s, j) => (j === i ? { ...s, key } : s)));
722
722
  const setSortDir = (i, dir) => setSorts(sorts.map((s, j) => (j === i ? { ...s, dir } : s)));
723
723
  const removeSort = (i) => setSorts(sorts.filter((_, j) => j !== i));
724
+ /**
725
+ * Keep the selected cell on screen.
726
+ *
727
+ * Focus stays on the scroll region, never on a cell, so the browser has
728
+ * nothing to scroll to: the arrow keys moved the outline and the viewport
729
+ * stayed put. Measured in Chrome on a 384px-tall region over 40 rows —
730
+ * twenty ArrowDowns left `scrollTop` at 0 with the selected cell 761px below
731
+ * the top edge, i.e. the user was driving a selection they could not see
732
+ * (#257).
733
+ *
734
+ * `nearest` on both axes: a cell already in view is left exactly where it
735
+ * is, which is what makes this safe to run after a click too.
736
+ *
737
+ * No `behavior` of our own — the page's `scroll-behavior` decides, and Attio
738
+ * jumps rather than glides — except under reduced motion, where an explicit
739
+ * "auto" is the only thing that beats a host page that set
740
+ * `scroll-behavior: smooth` on the scroller (#208's rule: JS motion reads
741
+ * the preference itself, because CSS cannot reach an explicit argument).
742
+ */
743
+ const reduceMotion = (0, core_1.usePrefersReducedMotion)();
744
+ React.useEffect(() => {
745
+ if (!selCell)
746
+ return;
747
+ const cell = scrollRef.current?.querySelector(`[data-cell="${CSS.escape(`${selCell.id}:${selCell.key}`)}"]`);
748
+ cell?.scrollIntoView({
749
+ block: "nearest",
750
+ inline: "nearest",
751
+ ...(reduceMotion ? { behavior: "auto" } : null),
752
+ });
753
+ }, [selCell, reduceMotion]);
724
754
  const commitEdit = (id, key, raw) => {
725
755
  const col = cols.find((c) => c.key === key);
726
756
  const numeric = col?.type === "number" || col?.type === "currency";
@@ -1069,7 +1099,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
1069
1099
  // row instead of a bare "182,000", and the same header is
1070
1100
  // repeated in every cell's context (#231).
1071
1101
  const Cell = (i === 0 ? "th" : "td");
1072
- return ((0, jsx_runtime_1.jsxs)(Cell, { scope: i === 0 ? "row" : undefined, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1102
+ return ((0, jsx_runtime_1.jsxs)(Cell, { scope: i === 0 ? "row" : undefined, "data-cell": `${row.id}:${col.key}`, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1073
1103
  ? ""
1074
1104
  : undefined, onClick: () => {
1075
1105
  if (isPicker)
@@ -63,8 +63,8 @@
63
63
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
64
64
  & table { table-layout: fixed; }
65
65
  /* column resize grip */
66
- & .datatable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
67
- & .datatable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: var(--bg-brand-solid); opacity: 0; transition: opacity .12s; }
66
+ & .datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
67
+ & .datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: var(--bg-brand-solid); opacity: 0; transition: opacity .12s; }
68
68
  & .datatable-resize:hover::after, & .datatable-resize[data-resizing]::after { opacity: 1; }
69
69
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
70
70
  & .datatable-entity { display: flex; align-items: center; gap: var(--space-1-5); min-width: 0; color: var(--fg-primary); font-weight: var(--weight-medium); }
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ArrowDown, ArrowUp, Check, ChevronsUpDown, GripVertical, Plus, Search, SlidersHorizontal, } from "lucide-react";
3
3
  import * as React from "react";
4
+ import { isRtlElement } from "../../core/index.js";
4
5
  import { Checkbox } from "../checkbox/index.js";
5
6
  import { Menu, MenuItem } from "../menu/index.js";
6
7
  import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow, } from "../table/index.js";
@@ -329,10 +330,13 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
329
330
  nr = Math.max(0, r - 1);
330
331
  else if (e.key === "ArrowDown")
331
332
  nr = Math.min(rows.length - 1, r + 1);
332
- else if (e.key === "ArrowLeft")
333
- nc = Math.max(0, c - 1);
334
- else if (e.key === "ArrowRight")
335
- nc = Math.min(columns.length - 1, c + 1);
333
+ // The arrows follow what the user SEES: under RTL the first column renders
334
+ // rightmost, so ArrowLeft steps forward and ArrowRight steps back (#255).
335
+ else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
336
+ const rtl = isRtlElement(e.currentTarget);
337
+ const forward = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
338
+ nc = forward ? Math.min(columns.length - 1, c + 1) : Math.max(0, c - 1);
339
+ }
336
340
  else if (e.key === "Escape") {
337
341
  setSelCell(null);
338
342
  return;
@@ -390,6 +394,11 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
390
394
  key: col.key,
391
395
  startX: e.clientX,
392
396
  startW: colWidth(col),
397
+ // The columns reverse under RTL, so the grip — anchored at
398
+ // `inset-inline-end` — lands on the column's LEFT edge there, and
399
+ // pulling it further left must GROW the column. `clientX` stays
400
+ // physical, so the sign is the other half of that conversion (#214).
401
+ dir: isRtlElement(e.currentTarget) ? -1 : 1,
393
402
  };
394
403
  setResizingKey(col.key);
395
404
  }
@@ -397,7 +406,7 @@ export function DataTable({ columns: columnsProp, data, selectable = false, sele
397
406
  const r = resizeRef.current;
398
407
  if (!r)
399
408
  return;
400
- const w = Math.max(64, r.startW + (e.clientX - r.startX));
409
+ const w = Math.max(64, r.startW + r.dir * (e.clientX - r.startX));
401
410
  setWidths((prev) => ({ ...prev, [r.key]: w }));
402
411
  }
403
412
  function endResize(e) {
@@ -70,8 +70,8 @@ const config = defineRecipe({
70
70
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
71
71
  & table { table-layout: fixed; }
72
72
  /* column resize grip */
73
- & .datatable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
74
- & .datatable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: ${c.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
73
+ & .datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
74
+ & .datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: ${c.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
75
75
  & .datatable-resize:hover::after, & .datatable-resize[data-resizing]::after { opacity: 1; }
76
76
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
77
77
  & .datatable-entity { display: flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; color: ${c.fgPrimary}; font-weight: ${c.weight.medium}; }
@@ -14,7 +14,7 @@ import { Menu, MenuItem, useMenuKeyboard } from "../../components/menu/index.js"
14
14
  import { MultiSelect } from "../../components/multiselect/index.js";
15
15
  import { Select } from "../../components/select/index.js";
16
16
  import { Switch } from "../../components/switch/index.js";
17
- import { isRtlElement } from "../../core/index.js";
17
+ import { isRtlElement, usePrefersReducedMotion } from "../../core/index.js";
18
18
  import { companyLogos } from "./company-logos.js";
19
19
  import { recordstableRecipe } from "./recordstable.recipe.js";
20
20
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
@@ -681,6 +681,36 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
681
681
  const setSortField = (i, key) => setSorts(sorts.map((s, j) => (j === i ? { ...s, key } : s)));
682
682
  const setSortDir = (i, dir) => setSorts(sorts.map((s, j) => (j === i ? { ...s, dir } : s)));
683
683
  const removeSort = (i) => setSorts(sorts.filter((_, j) => j !== i));
684
+ /**
685
+ * Keep the selected cell on screen.
686
+ *
687
+ * Focus stays on the scroll region, never on a cell, so the browser has
688
+ * nothing to scroll to: the arrow keys moved the outline and the viewport
689
+ * stayed put. Measured in Chrome on a 384px-tall region over 40 rows —
690
+ * twenty ArrowDowns left `scrollTop` at 0 with the selected cell 761px below
691
+ * the top edge, i.e. the user was driving a selection they could not see
692
+ * (#257).
693
+ *
694
+ * `nearest` on both axes: a cell already in view is left exactly where it
695
+ * is, which is what makes this safe to run after a click too.
696
+ *
697
+ * No `behavior` of our own — the page's `scroll-behavior` decides, and Attio
698
+ * jumps rather than glides — except under reduced motion, where an explicit
699
+ * "auto" is the only thing that beats a host page that set
700
+ * `scroll-behavior: smooth` on the scroller (#208's rule: JS motion reads
701
+ * the preference itself, because CSS cannot reach an explicit argument).
702
+ */
703
+ const reduceMotion = usePrefersReducedMotion();
704
+ React.useEffect(() => {
705
+ if (!selCell)
706
+ return;
707
+ const cell = scrollRef.current?.querySelector(`[data-cell="${CSS.escape(`${selCell.id}:${selCell.key}`)}"]`);
708
+ cell?.scrollIntoView({
709
+ block: "nearest",
710
+ inline: "nearest",
711
+ ...(reduceMotion ? { behavior: "auto" } : null),
712
+ });
713
+ }, [selCell, reduceMotion]);
684
714
  const commitEdit = (id, key, raw) => {
685
715
  const col = cols.find((c) => c.key === key);
686
716
  const numeric = col?.type === "number" || col?.type === "currency";
@@ -1029,7 +1059,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
1029
1059
  // row instead of a bare "182,000", and the same header is
1030
1060
  // repeated in every cell's context (#231).
1031
1061
  const Cell = (i === 0 ? "th" : "td");
1032
- return (_jsxs(Cell, { scope: i === 0 ? "row" : undefined, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1062
+ return (_jsxs(Cell, { scope: i === 0 ? "row" : undefined, "data-cell": `${row.id}:${col.key}`, className: `recordstable-td${i === 0 ? " recordstable-col-entity" : ""}${canEdit ? " recordstable-td--editable" : ""}${isPicker ? " recordstable-td--picker" : ""}`, "data-enriched": col.enriched ? "" : undefined, "data-selected": selCell?.id === row.id && selCell?.key === col.key
1033
1063
  ? ""
1034
1064
  : undefined, onClick: () => {
1035
1065
  if (isPicker)
package/dist/styles.css CHANGED
@@ -522,8 +522,8 @@
522
522
  /* fixed layout so column widths (and resize) are exact — like RecordsTable */
523
523
  & table { table-layout: fixed; }
524
524
  /* column resize grip */
525
- & .datatable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
526
- & .datatable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: var(--bg-brand-solid); opacity: 0; transition: opacity .12s; }
525
+ & .datatable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
526
+ & .datatable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: var(--bg-brand-solid); opacity: 0; transition: opacity .12s; }
527
527
  & .datatable-resize:hover::after, & .datatable-resize[data-resizing]::after { opacity: 1; }
528
528
  /* typed cells — entity (avatar + name), colour-coded tags, numbers, dates */
529
529
  & .datatable-entity { display: flex; align-items: center; gap: var(--space-1-5); min-width: 0; color: var(--fg-primary); font-weight: var(--weight-medium); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.31.1",
3
+ "version": "0.31.3",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",