@crisp-ui-kit/crisp 0.20.0 → 0.20.2

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/appshell.css CHANGED
@@ -3,8 +3,8 @@
3
3
  display: flex; height: 100%; width: 100%; overflow: hidden; background: var(--bg-surface); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
4
4
  & .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
5
5
  & .appshell-side-inner { height: 100%; }
6
- & .appshell-resize { position: absolute; top: 0; right: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
7
- & .appshell-resize::after { content: ""; position: absolute; top: 0; right: 0; width: 1px; height: 100%; background: var(--border-subtle); transition: background-color .16s, height .16s; }
6
+ & .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
7
+ & .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: var(--border-subtle); transition: background-color .16s, height .16s; }
8
8
  & .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: var(--bg-brand-solid); }
9
9
  & .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
10
10
  & .appshell-expand { position: absolute; top: var(--space-2); inset-inline-start: var(--space-2); z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: var(--size-row); height: var(--size-row); padding: 0; border: none; border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--border-subtle); background: var(--bg-surface); color: var(--fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRtlElement = isRtlElement;
4
+ /**
5
+ * The nearest declared direction. The `dir` attribute is checked first because
6
+ * it is how a document says this in practice and it is readable without a
7
+ * layout, then computed style for a direction set purely in CSS.
8
+ *
9
+ * Shared so that a floating layer's edge choice and a drag handler's sign come
10
+ * from the same answer. They have to agree: mirroring one without the other is
11
+ * the half-conversion lesson 54 is about.
12
+ */
13
+ function isRtlElement(anchor) {
14
+ if (!anchor || typeof window === "undefined")
15
+ return false;
16
+ const declared = anchor.closest("[dir]")?.getAttribute("dir");
17
+ if (declared)
18
+ return declared.toLowerCase() === "rtl";
19
+ try {
20
+ return window.getComputedStyle(anchor).direction === "rtl";
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
@@ -14,11 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.defineRecipe = exports.cx = void 0;
17
+ exports.usePrefersReducedMotion = exports.resolveRecipe = exports.Primitive = exports.placeFloating = exports.isRtlElement = exports.defineRecipe = exports.cx = void 0;
18
18
  var cx_1 = require("./cx");
19
19
  Object.defineProperty(exports, "cx", { enumerable: true, get: function () { return cx_1.cx; } });
20
20
  var define_1 = require("./define");
21
21
  Object.defineProperty(exports, "defineRecipe", { enumerable: true, get: function () { return define_1.defineRecipe; } });
22
+ var direction_1 = require("./direction");
23
+ Object.defineProperty(exports, "isRtlElement", { enumerable: true, get: function () { return direction_1.isRtlElement; } });
22
24
  var place_floating_1 = require("./place-floating");
23
25
  Object.defineProperty(exports, "placeFloating", { enumerable: true, get: function () { return place_floating_1.placeFloating; } });
24
26
  var primitive_1 = require("./primitive");
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.placeFloating = placeFloating;
4
+ const direction_1 = require("./direction");
4
5
  /**
5
6
  * Where a floating layer goes, in logical terms.
6
7
  *
@@ -21,26 +22,8 @@ exports.placeFloating = placeFloating;
21
22
  * be the half-conversion #146 exists to prevent — the rect it is measured
22
23
  * against would still be physical.
23
24
  */
24
- /**
25
- * The nearest declared direction. The `dir` attribute is checked first because
26
- * it is how a document says this in practice and it is readable without a
27
- * layout, then computed style for a direction set purely in CSS.
28
- */
29
- function isRtl(anchor) {
30
- if (!anchor || typeof window === "undefined")
31
- return false;
32
- const declared = anchor.closest("[dir]")?.getAttribute("dir");
33
- if (declared)
34
- return declared.toLowerCase() === "rtl";
35
- try {
36
- return window.getComputedStyle(anchor).direction === "rtl";
37
- }
38
- catch {
39
- return false;
40
- }
41
- }
42
25
  function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }) {
43
- const rtl = isRtl(anchor);
26
+ const rtl = (0, direction_1.isRtlElement)(anchor);
44
27
  const up = window.innerHeight - rect.bottom < flipBelow;
45
28
  const style = up
46
29
  ? { bottom: window.innerHeight - rect.top + gap }
@@ -37,6 +37,7 @@ exports.AppShell = AppShell;
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 appshell_recipe_1 = require("./appshell.recipe");
41
42
  /**
42
43
  * App-shell layout with a collapsible + resizable sidebar, mirroring Attio's
@@ -62,6 +63,11 @@ function AppShell({ sidebar, children, width: widthProp, defaultWidth = 275, onW
62
63
  e.preventDefault();
63
64
  const startX = e.clientX;
64
65
  const startW = width;
66
+ // The sidebar is the flex row's first child, so under RTL it renders on the
67
+ // RIGHT — measured. `clientX` stays physical, so pulling right must SHRINK
68
+ // it there. Mirroring the handle's CSS without this is the half-conversion
69
+ // lesson 54 is about, which is why both change in one commit (#212).
70
+ const dir = (0, core_1.isRtlElement)(e.currentTarget) ? -1 : 1;
65
71
  // Attio only enters the drag state once the pointer has actually moved — a
66
72
  // click on the handle is not a drag, so the blue bar must not flash on press.
67
73
  let moved = false;
@@ -75,7 +81,7 @@ function AppShell({ sidebar, children, width: widthProp, defaultWidth = 275, onW
75
81
  moved = true;
76
82
  setDragging(true);
77
83
  }
78
- const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + (ev.clientX - startX))));
84
+ const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + dir * (ev.clientX - startX))));
79
85
  setDragWidth(w);
80
86
  last = w;
81
87
  };
@@ -12,8 +12,8 @@ const config = (0, core_1.defineRecipe)({
12
12
  display: flex; height: 100%; width: 100%; overflow: hidden; background: ${tokens_1.contract.bgSurface}; font-family: ${tokens_1.contract.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
13
13
  & .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
14
14
  & .appshell-side-inner { height: 100%; }
15
- & .appshell-resize { position: absolute; top: 0; right: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
16
- & .appshell-resize::after { content: ""; position: absolute; top: 0; right: 0; width: 1px; height: 100%; background: ${tokens_1.contract.borderSubtle}; transition: background-color .16s, height .16s; }
15
+ & .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
16
+ & .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: ${tokens_1.contract.borderSubtle}; transition: background-color .16s, height .16s; }
17
17
  & .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: ${tokens_1.contract.bgBrandSolid}; }
18
18
  & .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
19
19
  & .appshell-expand { position: absolute; top: ${tokens_1.contract.space["2"]}; inset-inline-start: ${tokens_1.contract.space["2"]}; z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: ${tokens_1.contract.size.row}; height: ${tokens_1.contract.size.row}; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.md}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderSubtle}; background: ${tokens_1.contract.bgSurface}; color: ${tokens_1.contract.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
@@ -42,6 +42,7 @@ const avatar_1 = require("../../components/avatar");
42
42
  const checkbox_1 = require("../../components/checkbox");
43
43
  const menu_1 = require("../../components/menu");
44
44
  const tabs_1 = require("../../components/tabs");
45
+ const core_1 = require("../../core");
45
46
  const recordstable_1 = require("../recordstable");
46
47
  const recordpage_recipe_1 = require("./recordpage.recipe");
47
48
  function valueEmpty(v) {
@@ -215,14 +216,20 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
215
216
  const [resizing, setResizing] = React.useState(false);
216
217
  const dragRef = React.useRef(null);
217
218
  const onResizeDown = (e) => {
218
- dragRef.current = { x: e.clientX, w: detailsW };
219
+ // details is the flex row's first child, so it sits on the right under RTL
220
+ // and a rightward pull must shrink it (#212)
221
+ dragRef.current = {
222
+ x: e.clientX,
223
+ w: detailsW,
224
+ dir: (0, core_1.isRtlElement)(e.currentTarget) ? -1 : 1,
225
+ };
219
226
  setResizing(true);
220
227
  e.currentTarget.setPointerCapture(e.pointerId);
221
228
  };
222
229
  const onResizeMove = (e) => {
223
230
  if (!dragRef.current)
224
231
  return;
225
- const next = dragRef.current.w + (e.clientX - dragRef.current.x);
232
+ const next = dragRef.current.w + dragRef.current.dir * (e.clientX - dragRef.current.x);
226
233
  setDetailsW(Math.max(300, Math.min(560, next)));
227
234
  };
228
235
  const onResizeUp = (e) => {
@@ -34,8 +34,8 @@ const config = (0, core_1.defineRecipe)({
34
34
 
35
35
  & .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid ${tokens_1.contract.borderSubtle}; padding: ${tokens_1.contract.space["3"]}; }
36
36
  & .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
37
- & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; left: -4px; width: 9px; }
38
- & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
37
+ & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
38
+ & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
39
39
  & .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, ${tokens_1.contract.bgBrandSolid} 55%, transparent); }
40
40
  & .recordpage-entity { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; padding: ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["1"]} ${tokens_1.contract.space["2"]}; }
41
41
  & .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: ${tokens_1.contract.bgRaised}; }
@@ -52,6 +52,7 @@ const menu_1 = require("../../components/menu");
52
52
  const multiselect_1 = require("../../components/multiselect");
53
53
  const select_1 = require("../../components/select");
54
54
  const switch_1 = require("../../components/switch");
55
+ const core_1 = require("../../core");
55
56
  const company_logos_1 = require("./company-logos");
56
57
  const recordstable_recipe_1 = require("./recordstable.recipe");
57
58
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
@@ -897,7 +898,11 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
897
898
  setOpenMenu(null);
898
899
  };
899
900
  // column resize via pointer drag on the header edge
900
- const resizeCol = (key, startX, startW) => {
901
+ const resizeCol = (key, startX, startW, grip) => {
902
+ // A table's columns reverse under RTL — measured — so the grip sits on the
903
+ // column's leading edge there and a rightward pull must SHRINK it. The
904
+ // anchor is `inset-inline-end`; this is the other half (#214).
905
+ const dir = (0, core_1.isRtlElement)(grip) ? -1 : 1;
901
906
  // guard the whole document during the drag: kill text selection and force a
902
907
  // resize cursor so the gesture doesn't select cells or flicker (was buggy)
903
908
  const prevSelect = document.body.style.userSelect;
@@ -905,7 +910,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
905
910
  document.body.style.userSelect = "none";
906
911
  document.body.style.cursor = "col-resize";
907
912
  const onMove = (e) => {
908
- const w = Math.round(Math.max(72, startW + (e.clientX - startX)));
913
+ const w = Math.round(Math.max(72, startW + dir * (e.clientX - startX)));
909
914
  setCols((prev) => prev.map((c) => (c.key === key ? { ...c, width: w } : c)));
910
915
  };
911
916
  const onUp = () => {
@@ -995,7 +1000,7 @@ function RecordsTable({ labels, columns: columnsProp = exports.defaultColumns, r
995
1000
  e.stopPropagation();
996
1001
  const th = e.currentTarget
997
1002
  .parentElement;
998
- resizeCol(col.key, e.clientX, th ? th.getBoundingClientRect().width : 180);
1003
+ resizeCol(col.key, e.clientX, th ? th.getBoundingClientRect().width : 180, e.currentTarget);
999
1004
  }, onClick: (e) => e.stopPropagation() })] }, col.key));
1000
1005
  }), (0, jsx_runtime_1.jsx)("th", { className: "recordstable-th recordstable-addcol", children: allowAddColumn && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: addColumn, "aria-label": t.addColumn, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }) })) })] }) }), (0, jsx_runtime_1.jsx)("tbody", { children: visibleRows.map((row) => ((0, jsx_runtime_1.jsxs)("tr", { "data-selected": selected.has(row.id) ? "" : undefined, onContextMenu: (e) => {
1001
1006
  e.preventDefault();
@@ -46,8 +46,8 @@ ${(0, tabletoolbar_chrome_1.toolbarChromeBody)("recordstable")}
46
46
  & .recordstable-th[data-sorted="desc"] .recordstable-sort svg { transform: rotate(180deg); }
47
47
 
48
48
  /* column resize handle */
49
- & .recordstable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
50
- & .recordstable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: ${tokens_1.contract.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
49
+ & .recordstable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
50
+ & .recordstable-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; }
51
51
  & .recordstable-resize:hover::after, & .recordstable-resize[data-resizing]::after { opacity: 1; }
52
52
 
53
53
  /* add-column header */
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The nearest declared direction. The `dir` attribute is checked first because
3
+ * it is how a document says this in practice and it is readable without a
4
+ * layout, then computed style for a direction set purely in CSS.
5
+ *
6
+ * Shared so that a floating layer's edge choice and a drag handler's sign come
7
+ * from the same answer. They have to agree: mirroring one without the other is
8
+ * the half-conversion lesson 54 is about.
9
+ */
10
+ export function isRtlElement(anchor) {
11
+ if (!anchor || typeof window === "undefined")
12
+ return false;
13
+ const declared = anchor.closest("[dir]")?.getAttribute("dir");
14
+ if (declared)
15
+ return declared.toLowerCase() === "rtl";
16
+ try {
17
+ return window.getComputedStyle(anchor).direction === "rtl";
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
@@ -1,5 +1,6 @@
1
1
  export { cx } from "./cx.js";
2
2
  export { defineRecipe } from "./define.js";
3
+ export { isRtlElement } from "./direction.js";
3
4
  export { placeFloating, } from "./place-floating.js";
4
5
  export { Primitive } from "./primitive.js";
5
6
  export { resolveRecipe } from "./resolve.js";
@@ -1,3 +1,4 @@
1
+ import { isRtlElement } from "./direction.js";
1
2
  /**
2
3
  * Where a floating layer goes, in logical terms.
3
4
  *
@@ -18,26 +19,8 @@
18
19
  * be the half-conversion #146 exists to prevent — the rect it is measured
19
20
  * against would still be physical.
20
21
  */
21
- /**
22
- * The nearest declared direction. The `dir` attribute is checked first because
23
- * it is how a document says this in practice and it is readable without a
24
- * layout, then computed style for a direction set purely in CSS.
25
- */
26
- function isRtl(anchor) {
27
- if (!anchor || typeof window === "undefined")
28
- return false;
29
- const declared = anchor.closest("[dir]")?.getAttribute("dir");
30
- if (declared)
31
- return declared.toLowerCase() === "rtl";
32
- try {
33
- return window.getComputedStyle(anchor).direction === "rtl";
34
- }
35
- catch {
36
- return false;
37
- }
38
- }
39
22
  export function placeFloating({ rect, align = "start", gap, flipBelow, anchor, }) {
40
- const rtl = isRtl(anchor);
23
+ const rtl = isRtlElement(anchor);
41
24
  const up = window.innerHeight - rect.bottom < flipBelow;
42
25
  const style = up
43
26
  ? { bottom: window.innerHeight - rect.top + gap }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PanelLeft } from "lucide-react";
3
3
  import * as React from "react";
4
+ import { isRtlElement } from "../../core/index.js";
4
5
  import { appshellRecipe } from "./appshell.recipe.js";
5
6
  /**
6
7
  * App-shell layout with a collapsible + resizable sidebar, mirroring Attio's
@@ -26,6 +27,11 @@ export function AppShell({ sidebar, children, width: widthProp, defaultWidth = 2
26
27
  e.preventDefault();
27
28
  const startX = e.clientX;
28
29
  const startW = width;
30
+ // The sidebar is the flex row's first child, so under RTL it renders on the
31
+ // RIGHT — measured. `clientX` stays physical, so pulling right must SHRINK
32
+ // it there. Mirroring the handle's CSS without this is the half-conversion
33
+ // lesson 54 is about, which is why both change in one commit (#212).
34
+ const dir = isRtlElement(e.currentTarget) ? -1 : 1;
29
35
  // Attio only enters the drag state once the pointer has actually moved — a
30
36
  // click on the handle is not a drag, so the blue bar must not flash on press.
31
37
  let moved = false;
@@ -39,7 +45,7 @@ export function AppShell({ sidebar, children, width: widthProp, defaultWidth = 2
39
45
  moved = true;
40
46
  setDragging(true);
41
47
  }
42
- const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + (ev.clientX - startX))));
48
+ const w = Math.round(Math.min(maxWidth, Math.max(minWidth, startW + dir * (ev.clientX - startX))));
43
49
  setDragWidth(w);
44
50
  last = w;
45
51
  };
@@ -9,8 +9,8 @@ const config = defineRecipe({
9
9
  display: flex; height: 100%; width: 100%; overflow: hidden; background: ${c.bgSurface}; font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
10
10
  & .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
11
11
  & .appshell-side-inner { height: 100%; }
12
- & .appshell-resize { position: absolute; top: 0; right: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
13
- & .appshell-resize::after { content: ""; position: absolute; top: 0; right: 0; width: 1px; height: 100%; background: ${c.borderSubtle}; transition: background-color .16s, height .16s; }
12
+ & .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
13
+ & .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: ${c.borderSubtle}; transition: background-color .16s, height .16s; }
14
14
  & .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: ${c.bgBrandSolid}; }
15
15
  & .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
16
16
  & .appshell-expand { position: absolute; top: ${c.space["2"]}; inset-inline-start: ${c.space["2"]}; z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: ${c.size.row}; height: ${c.size.row}; padding: 0; border: none; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; background: ${c.bgSurface}; color: ${c.fgMuted}; font-family: inherit; cursor: pointer; transition: background-color .12s; }
@@ -5,6 +5,7 @@ import { Avatar } from "../../components/avatar/index.js";
5
5
  import { Checkbox } from "../../components/checkbox/index.js";
6
6
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
7
7
  import { Tabs } from "../../components/tabs/index.js";
8
+ import { isRtlElement } from "../../core/index.js";
8
9
  import { tagTone, } from "../recordstable/index.js";
9
10
  import { recordpageRecipe } from "./recordpage.recipe.js";
10
11
  function valueEmpty(v) {
@@ -178,14 +179,20 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
178
179
  const [resizing, setResizing] = React.useState(false);
179
180
  const dragRef = React.useRef(null);
180
181
  const onResizeDown = (e) => {
181
- dragRef.current = { x: e.clientX, w: detailsW };
182
+ // details is the flex row's first child, so it sits on the right under RTL
183
+ // and a rightward pull must shrink it (#212)
184
+ dragRef.current = {
185
+ x: e.clientX,
186
+ w: detailsW,
187
+ dir: isRtlElement(e.currentTarget) ? -1 : 1,
188
+ };
182
189
  setResizing(true);
183
190
  e.currentTarget.setPointerCapture(e.pointerId);
184
191
  };
185
192
  const onResizeMove = (e) => {
186
193
  if (!dragRef.current)
187
194
  return;
188
- const next = dragRef.current.w + (e.clientX - dragRef.current.x);
195
+ const next = dragRef.current.w + dragRef.current.dir * (e.clientX - dragRef.current.x);
189
196
  setDetailsW(Math.max(300, Math.min(560, next)));
190
197
  };
191
198
  const onResizeUp = (e) => {
@@ -31,8 +31,8 @@ const config = defineRecipe({
31
31
 
32
32
  & .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid ${c.borderSubtle}; padding: ${c.space["3"]}; }
33
33
  & .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
34
- & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; left: -4px; width: 9px; }
35
- & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
34
+ & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
35
+ & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
36
36
  & .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, ${c.bgBrandSolid} 55%, transparent); }
37
37
  & .recordpage-entity { display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; }
38
38
  & .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: ${c.bgRaised}; }
@@ -14,6 +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
18
  import { companyLogos } from "./company-logos.js";
18
19
  import { recordstableRecipe } from "./recordstable.recipe.js";
19
20
  const EDITABLE = new Set(["text", "number", "url", "currency"]);
@@ -859,7 +860,11 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
859
860
  setOpenMenu(null);
860
861
  };
861
862
  // column resize via pointer drag on the header edge
862
- const resizeCol = (key, startX, startW) => {
863
+ const resizeCol = (key, startX, startW, grip) => {
864
+ // A table's columns reverse under RTL — measured — so the grip sits on the
865
+ // column's leading edge there and a rightward pull must SHRINK it. The
866
+ // anchor is `inset-inline-end`; this is the other half (#214).
867
+ const dir = isRtlElement(grip) ? -1 : 1;
863
868
  // guard the whole document during the drag: kill text selection and force a
864
869
  // resize cursor so the gesture doesn't select cells or flicker (was buggy)
865
870
  const prevSelect = document.body.style.userSelect;
@@ -867,7 +872,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
867
872
  document.body.style.userSelect = "none";
868
873
  document.body.style.cursor = "col-resize";
869
874
  const onMove = (e) => {
870
- const w = Math.round(Math.max(72, startW + (e.clientX - startX)));
875
+ const w = Math.round(Math.max(72, startW + dir * (e.clientX - startX)));
871
876
  setCols((prev) => prev.map((c) => (c.key === key ? { ...c, width: w } : c)));
872
877
  };
873
878
  const onUp = () => {
@@ -957,7 +962,7 @@ export function RecordsTable({ labels, columns: columnsProp = defaultColumns, ro
957
962
  e.stopPropagation();
958
963
  const th = e.currentTarget
959
964
  .parentElement;
960
- resizeCol(col.key, e.clientX, th ? th.getBoundingClientRect().width : 180);
965
+ resizeCol(col.key, e.clientX, th ? th.getBoundingClientRect().width : 180, e.currentTarget);
961
966
  }, onClick: (e) => e.stopPropagation() })] }, col.key));
962
967
  }), _jsx("th", { className: "recordstable-th recordstable-addcol", children: allowAddColumn && (_jsx("button", { type: "button", onClick: addColumn, "aria-label": t.addColumn, children: _jsx(Plus, { size: 15, strokeWidth: 1.75 }) })) })] }) }), _jsx("tbody", { children: visibleRows.map((row) => (_jsxs("tr", { "data-selected": selected.has(row.id) ? "" : undefined, onContextMenu: (e) => {
963
968
  e.preventDefault();
@@ -43,8 +43,8 @@ ${toolbarChromeBody("recordstable")}
43
43
  & .recordstable-th[data-sorted="desc"] .recordstable-sort svg { transform: rotate(180deg); }
44
44
 
45
45
  /* column resize handle */
46
- & .recordstable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
47
- & .recordstable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: ${c.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
46
+ & .recordstable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
47
+ & .recordstable-resize::after { content: ""; position: absolute; top: 0; inset-inline-start: 3px; width: 2px; height: 100%; background: ${c.bgBrandSolid}; opacity: 0; transition: opacity .12s; }
48
48
  & .recordstable-resize:hover::after, & .recordstable-resize[data-resizing]::after { opacity: 1; }
49
49
 
50
50
  /* add-column header */
@@ -22,8 +22,8 @@
22
22
 
23
23
  & .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid var(--border-subtle); padding: var(--space-3); }
24
24
  & .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
25
- & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; left: -4px; width: 9px; }
26
- & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
25
+ & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
26
+ & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
27
27
  & .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, var(--bg-brand-solid) 55%, transparent); }
28
28
  & .recordpage-entity { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-1) var(--space-2); }
29
29
  & .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: var(--bg-raised); }
@@ -91,8 +91,8 @@
91
91
  & .recordstable-th[data-sorted="desc"] .recordstable-sort svg { transform: rotate(180deg); }
92
92
 
93
93
  /* column resize handle */
94
- & .recordstable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
95
- & .recordstable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: var(--bg-brand-solid); opacity: 0; transition: opacity .12s; }
94
+ & .recordstable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
95
+ & .recordstable-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; }
96
96
  & .recordstable-resize:hover::after, & .recordstable-resize[data-resizing]::after { opacity: 1; }
97
97
 
98
98
  /* add-column header */
package/dist/styles.css CHANGED
@@ -1857,8 +1857,8 @@
1857
1857
  display: flex; height: 100%; width: 100%; overflow: hidden; background: var(--bg-surface); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
1858
1858
  & .appshell-side { position: relative; flex-shrink: 0; overflow: hidden; }
1859
1859
  & .appshell-side-inner { height: 100%; }
1860
- & .appshell-resize { position: absolute; top: 0; right: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
1861
- & .appshell-resize::after { content: ""; position: absolute; top: 0; right: 0; width: 1px; height: 100%; background: var(--border-subtle); transition: background-color .16s, height .16s; }
1860
+ & .appshell-resize { position: absolute; top: 0; inset-inline-end: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 5; touch-action: none; }
1861
+ & .appshell-resize::after { content: ""; position: absolute; top: 0; inset-inline-end: 0; width: 1px; height: 100%; background: var(--border-subtle); transition: background-color .16s, height .16s; }
1862
1862
  & .appshell-resize:hover::after, & .appshell-resize[data-dragging]::after { width: 2px; background: var(--bg-brand-solid); }
1863
1863
  & .appshell-main { position: relative; flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
1864
1864
  & .appshell-expand { position: absolute; top: var(--space-2); inset-inline-start: var(--space-2); z-index: 6; display: inline-flex; align-items: center; justify-content: center; width: var(--size-row); height: var(--size-row); padding: 0; border: none; border-radius: var(--radius-md); box-shadow: inset 0 0 0 1px var(--border-subtle); background: var(--bg-surface); color: var(--fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
@@ -2213,8 +2213,8 @@
2213
2213
 
2214
2214
  & .recordpage-details { width: 320px; flex-shrink: 0; overflow-y: auto; border-inline-end: 1px solid var(--border-subtle); padding: var(--space-3); }
2215
2215
  & .recordpage-resizer { position: relative; flex: 0 0 auto; width: 0; align-self: stretch; cursor: col-resize; z-index: 5; touch-action: none; }
2216
- & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; left: -4px; width: 9px; }
2217
- & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; left: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
2216
+ & .recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
2217
+ & .recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
2218
2218
  & .recordpage-resizer:hover::after, & .recordpage-resizer[data-active]::after { background: color-mix(in srgb, var(--bg-brand-solid) 55%, transparent); }
2219
2219
  & .recordpage-entity { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-1) var(--space-1) var(--space-2); }
2220
2220
  & .recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: var(--bg-raised); }
@@ -2483,8 +2483,8 @@
2483
2483
  & .recordstable-th[data-sorted="desc"] .recordstable-sort svg { transform: rotate(180deg); }
2484
2484
 
2485
2485
  /* column resize handle */
2486
- & .recordstable-resize { position: absolute; top: 0; right: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
2487
- & .recordstable-resize::after { content: ""; position: absolute; top: 0; left: 3px; width: 2px; height: 100%; background: var(--bg-brand-solid); opacity: 0; transition: opacity .12s; }
2486
+ & .recordstable-resize { position: absolute; top: 0; inset-inline-end: -3px; width: 9px; height: 100%; cursor: col-resize; z-index: 6; touch-action: none; }
2487
+ & .recordstable-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; }
2488
2488
  & .recordstable-resize:hover::after, & .recordstable-resize[data-resizing]::after { opacity: 1; }
2489
2489
 
2490
2490
  /* add-column header */
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The nearest declared direction. The `dir` attribute is checked first because
3
+ * it is how a document says this in practice and it is readable without a
4
+ * layout, then computed style for a direction set purely in CSS.
5
+ *
6
+ * Shared so that a floating layer's edge choice and a drag handler's sign come
7
+ * from the same answer. They have to agree: mirroring one without the other is
8
+ * the half-conversion lesson 54 is about.
9
+ */
10
+ export declare function isRtlElement(anchor: Element | null | undefined): boolean;
@@ -1,5 +1,6 @@
1
1
  export { cx } from "./cx";
2
2
  export { defineRecipe } from "./define";
3
+ export { isRtlElement } from "./direction";
3
4
  export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
4
5
  export { Primitive } from "./primitive";
5
6
  export { resolveRecipe } from "./resolve";
@@ -17,4 +17,24 @@ export type PlacedFloating = {
17
17
  /** True when the layer was flipped above the trigger. */
18
18
  up: boolean;
19
19
  };
20
+ /**
21
+ * Where a floating layer goes, in logical terms.
22
+ *
23
+ * `start` and `end` are inline edges, not left and right. `Menu` and `Popover`
24
+ * both took an `align` prop named that way and then wrote
25
+ * `align === "end" ? right : left` with no direction anywhere, so in an RTL
26
+ * document both were backwards and a layer near the viewport edge overflowed on
27
+ * the wrong side (#210).
28
+ *
29
+ * The direction is read from the anchor's computed style rather than from a
30
+ * prop, so an inherited `dir` on any ancestor is honoured without every call
31
+ * site having to thread it down.
32
+ *
33
+ * Physical `left`/`right` are deliberate in the OUTPUT: this positions a
34
+ * `position: fixed` layer against viewport coordinates, which are physical in
35
+ * both directions. What must be direction-aware is the CHOICE of edge, and
36
+ * that happens here, once. Converting the output to `inset-inline-start` would
37
+ * be the half-conversion #146 exists to prevent — the rect it is measured
38
+ * against would still be physical.
39
+ */
20
40
  export declare function placeFloating({ rect, align, gap, flipBelow, anchor, }: PlaceFloatingOptions): PlacedFloating;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crisp-ui-kit/crisp",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "A lean React design system, measured pixel-for-pixel from Attio.",
5
5
  "license": "MIT",
6
6
  "type": "module",